diff --git a/catalog/Frameworks/Official-DotNet-ASPNet/skills/dotnet-webapi/SKILL.md b/catalog/Frameworks/Official-DotNet-ASPNet/skills/dotnet-webapi/SKILL.md new file mode 100644 index 0000000..c22ba5c --- /dev/null +++ b/catalog/Frameworks/Official-DotNet-ASPNet/skills/dotnet-webapi/SKILL.md @@ -0,0 +1,506 @@ +--- +name: dotnet-webapi +description: > + Guides creation and modification of ASP.NET Core Web API endpoints with + correct HTTP semantics, OpenAPI metadata, and error handling. + USE FOR: adding new API endpoints (controllers or minimal APIs), wiring up + OpenAPI/Swagger, creating .http test files, setting up global error handling + middleware. + DO NOT USE FOR: general C# coding style, EF Core data access or query + optimization (use optimizing-ef-core-queries), frontend/Blazor work, gRPC + services, or SignalR hubs. +license: MIT +--- + +# ASP.NET Core Web API + +Produce well-structured ASP.NET Core Web API endpoints with proper HTTP +semantics, OpenAPI documentation, and error handling. + +## When to Use + +Use this skill when working on ASP.NET Core HTTP APIs, including: + +- adding or modifying Web API endpoints implemented with controllers or minimal APIs; +- wiring up OpenAPI/Swagger metadata and endpoint documentation; +- defining request/response DTOs and consistent HTTP status code behavior; +- adding `.http` files or similar request-based API testing artifacts; +- configuring centralized API error handling middleware or exception mapping. + +## When Not to Use + +Do not use this skill for: + +- general C# coding style or non-API refactoring; +- EF Core data modeling or query optimization work; use `optimizing-ef-core-queries`; +- frontend, Razor, or Blazor UI changes; +- gRPC services; +- SignalR hubs or real-time messaging flows. + +## Inputs / prerequisites + +Before applying this skill, gather the project context needed to match the +existing API style and wiring: + +- the ASP.NET Core entry point, typically `Program.cs`; +- any existing controllers, especially classes inheriting `ControllerBase` or + using `[ApiController]`; +- any existing minimal API registrations such as `app.MapGet`, `app.MapPost`, + `app.MapPut`, or `app.MapDelete`; +- related DTO, model, validation, and error-handling types already used by the project; +- available build, run, and test commands so changes can be verified. + +If the user asks for a new endpoint, inspect the current project structure first +so the implementation follows the established conventions rather than mixing styles. +## Workflow + +### Step 1: Determine the API style + +Scan the project for existing endpoint patterns before writing any code. + +1. Search for classes inheriting `ControllerBase` or decorated with `[ApiController]`. +2. Search `Program.cs` or endpoint files for `app.MapGet`, `app.MapPost`, etc. +3. If the project already uses **controllers**, continue with controllers. +4. If the project already uses **minimal APIs**, continue with minimal APIs. +5. If neither exists (new project), **default to minimal APIs** unless the user + explicitly requests controllers. + +Do not mix styles in the same project. + +### Step 2: Define request and response types + +Create dedicated types for API input and output. Never expose EF Core entities +directly in request or response bodies. + +**Use `sealed record` for all DTOs.** Records enforce immutability, provide +value-based equality, and produce concise code. Seal them to prevent unintended +inheritance and enable JIT devirtualization (CA1852). + +**Naming convention:** + +| Role | Convention | Example | +|------|-----------|---------| +| Input (create) | `Create{Entity}Request` | `CreateProductRequest` | +| Input (update) | `Update{Entity}Request` | `UpdateProductRequest` | +| Output (single) | `{Entity}Response` | `ProductResponse` | +| Output (list) | `{Entity}ListResponse` | `ProductListResponse` | + +**XML doc comments on all DTOs:** Add `` XML doc comments to every +request and response type exposed in the API. These comments are automatically +included in the generated OpenAPI specification, producing richer documentation +without extra metadata calls. + +Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments + +**Date and time values — use `DateTimeOffset`:** When a DTO includes a date or +time property, always use `DateTimeOffset` instead of `DateTime`. +`DateTimeOffset` preserves the UTC offset, avoids ambiguous timezone +conversions, and serializes to ISO 8601 with offset information in JSON — which +is what API consumers expect. + +Reference: https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset +**JSON serialization options — preserve existing behavior by default:** For +existing APIs, do **not** introduce stricter serialization/deserialization settings +unless the project already uses them or the user explicitly asks for them. Settings +such as case-sensitive property matching and strict number handling can break +existing clients. For **new projects**, or when strict JSON handling is explicitly +requested, configure options like the following to minimize the potential of +processing malicious requests: + +```csharp +// Apply these settings only for new projects, when the existing project already +// uses them, or when the user explicitly requests stricter JSON behavior. +builder.Services.ConfigureHttpJsonOptions(options => +{ + // disallow reading numbers from JSON strings + options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; + // match properties with exact casing during deserialization + options.SerializerOptions.PropertyNameCaseInsensitive = false; + // reject duplicate JSON property names during deserialization + options.SerializerOptions.AllowDuplicateProperties = false; + // omit null properties from serialized output + options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); +``` +**Enum properties — serialize as strings by default:** Unless the user +explicitly requests integer serialization, all enum properties should be +serialized as strings. String-serialized enums are human-readable, less fragile +when values are reordered, and produce better OpenAPI documentation. See Step 4 +for the `JsonStringEnumConverter` configuration. + +**Response DTOs** — use positional sealed records for concise, immutable output: + +```csharp +/// Represents a product returned by the API. +public sealed record ProductResponse( + int Id, + string Name, + decimal Price, + Category Category, + bool IsAvailable, + DateTimeOffset CreatedAt); +``` + +**Request DTOs** — use sealed records with `init` properties so data annotations +work naturally: + +```csharp +/// Payload for creating a new product. +public sealed record CreateProductRequest +{ + [Required, MaxLength(200)] + public required string Name { get; init; } + + [Range(0.01, 999999.99)] + public required decimal Price { get; init; } + + public required Category Category { get; init; } +} +``` + +Follow the same pattern for `Update{Entity}Request` records, adding any +additional properties the update requires (e.g., `IsAvailable`). + +**Minimal API validation — register explicitly:** Data-annotation validation +(`[Required]`, `[MaxLength]`, `[Range]`, etc.) is automatic in MVC controllers, +but minimal APIs require explicit opt-in. For **.NET 10+** projects using minimal +APIs, add the validation services in `Program.cs`: + +```csharp +builder.Services.AddValidation(); +``` + +This wires up an endpoint filter that validates parameters decorated with data +annotations before the handler executes, returning a `400 Bad Request` with a +validation problem details response on failure. + +Reference: https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0 + +**Do not** use mutable classes (`{ get; set; }`) for DTOs. Mutable DTOs allow +accidental modification after construction and lose the self-documenting +immutability that records provide. + +### Step 3: Implement the endpoints + +Whether using controllers or minimal APIs, follow these HTTP conventions +consistently. + +**Organizing minimal API endpoints:** For projects using minimal APIs, organize +endpoints by resource using static classes with a static `Map` method. +This pattern keeps endpoint definitions grouped by resource type, making the +code more maintainable and easier to navigate as the API grows. + +**Pattern structure:** + +1. Create one static class per resource (e.g., `ProductEndpoints`, `CategoryEndpoints`). +2. Define a static `Map(this WebApplication app)` extension method. +3. Inside the method, call `MapGet`, `MapPost`, `MapPut`, `MapDelete`, etc. for + that resource's endpoints. +4. In `Program.cs`, call each resource's `Map` method in order. + +**Minimal API return types — prefer `TypedResults`:** + +Always prefer `TypedResults` over the `Results` factory. `TypedResults` embeds +response type information in the method signature, giving the OpenAPI generator +richer metadata automatically. + +When a handler returns **multiple result types** (e.g., `Ok` or `NotFound`), +annotate the lambda with an explicit `Results` return type. This +lets you use `TypedResults` while still giving the compiler a common type: + +```csharp +async Task, NotFound>> (int id, ...) => ... +``` + +**Do not** use `TypedResults.Ok(x)` and `TypedResults.NotFound()` in a bare +ternary without an explicit return type annotation. `Ok` and `NotFound` are +different types with no common base the compiler can infer, which causes +`CS1593: Delegate 'RequestDelegate' does not take N arguments` because the +compiler falls back to matching `RequestDelegate(HttpContext)`. + +**Fallback — `Results` factory:** If a handler has many conditional branches +(7+ result types), you may use the `Results` factory (`Results.Ok()`, +`Results.NotFound()`) which returns `IResult`, sacrificing compile-time OpenAPI +inference for simpler signatures. + +**Status codes:** + +| Operation | Success | Common errors | +|-----------|---------|---------------| +| GET (single) | `200 OK` | `404 Not Found` | +| GET (list) | `200 OK` | — | +| POST (create) | `201 Created` with `Location` header | `400 Bad Request`, `409 Conflict` | +| PUT (full update) | `200 OK` | `400 Bad Request`, `404 Not Found` | +| PATCH (partial/action) | `200 OK` | `400 Bad Request`, `404 Not Found` | +| DELETE | `204 No Content` | `404 Not Found`, `409 Conflict` | + +**POST 201 responses:** Always return a `Location` header pointing to the +newly created resource. + +- Controllers: use `CreatedAtAction(nameof(GetById), new { id = ... }, response)` +- Minimal APIs: use `TypedResults.Created($"/api/products/{id}", response)` + +**CancellationToken:** Accept `CancellationToken` in every endpoint signature +and forward it through to all async calls (service methods, EF Core queries, +`HttpClient` calls). This allows the server to stop work when a client +disconnects. + +```csharp +// Controller example +[HttpGet("{id}")] +public async Task> GetById( + int id, CancellationToken cancellationToken) +{ + var product = await _productService.GetByIdAsync(id, cancellationToken); + return product is null ? NotFound() : Ok(product); +} + +// Minimal API example — TypedResults with explicit return type (recommended) +app.MapGet("/api/products/{id}", async Task, NotFound>> ( + int id, IProductService service, CancellationToken cancellationToken) => +{ + var product = await service.GetByIdAsync(id, cancellationToken); + return product is null ? TypedResults.NotFound() : TypedResults.Ok(product); +}); +``` + +### Step 4: Wire up OpenAPI + +Every ASP.NET Core Web API should have OpenAPI documentation. Check whether +the project already has OpenAPI configured before adding it. + +**For .NET 9+ projects**, use the built-in ASP.NET Core OpenAPI support +(`builder.Services.AddOpenApi()` + `app.MapOpenApi()` in development). +This is all that is needed — no additional packages required. + +**Do NOT add any `Swashbuckle.*` NuGet package** (`Swashbuckle.AspNetCore`, +`Swashbuckle.AspNetCore.SwaggerUI`, `Swashbuckle.AspNetCore.SwaggerGen`, +etc.) to .NET 9+ projects. Swashbuckle has known compatibility issues with +.NET 9+ and .NET 10 OpenAPI types. For projects targeting .NET 8 or earlier, +Swashbuckle is acceptable. If the project already has Swashbuckle installed, +keep it unless the user asks to remove it. + +Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview + +**OpenAPI metadata on endpoints:** Add descriptive metadata so the generated +documentation is useful, not just a list of routes. For minimal APIs, chain +the metadata methods: + +```csharp +app.MapGet("/api/products/{id}", handler) + .WithName("GetProductById") + .WithSummary("Get a product by ID") + .WithDescription("Returns the full product details including category.") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); +``` + +**Enum serialization (strings by default):** Configure JSON serialization so +enums appear as readable strings in both API responses and OpenAPI schemas. +Always add this configuration unless the user explicitly requests integer +enum serialization. Configure it for both minimal APIs and controllers, as +they use different option types: + +```csharp +// Minimal APIs +builder.Services.ConfigureHttpJsonOptions(options => + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); + +// Controllers / MVC +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); +``` + +### Step 5: Set up error handling + +Use a global exception handler so that individual endpoints do not need +try-catch blocks. Return RFC 7807 Problem Details for all error responses. + +**For .NET 8+ projects**, prefer the built-in exception handler middleware: + +```csharp +builder.Services.AddProblemDetails(); + +app.UseExceptionHandler(); +app.UseStatusCodePages(); +``` + +If the project needs custom exception-to-status-code mapping (e.g., a +`NotFoundException` should return 404), implement `IExceptionHandler`: + +```csharp +internal sealed class ApiExceptionHandler(ILogger logger) + : IExceptionHandler +{ + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + var (statusCode, title) = exception switch + { + KeyNotFoundException => (StatusCodes.Status404NotFound, "Not Found"), + ArgumentException => (StatusCodes.Status400BadRequest, "Bad Request"), + InvalidOperationException => (StatusCodes.Status409Conflict, "Conflict"), + _ => (0, (string?)null) + }; + + if (statusCode == 0) + return false; // Let the default handler deal with it + + // Important: returning true below suppresses the exception diagnostics middleware + // for this exception, so ensure it is logged/telemetrized before returning. + logger.LogWarning(exception, "Handled API exception: {Title}", title); + + httpContext.Response.StatusCode = statusCode; + await httpContext.Response.WriteAsJsonAsync(new ProblemDetails + { + Status = statusCode, + Title = title, + // Do not use exception.Message here — it may leak sensitive internal details. + // Use a safe, user-facing message instead. + Detail = title, + Instance = httpContext.Request.Path + }, cancellationToken); + + return true; + } +} +``` + +Register it: + +```csharp +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +app.UseExceptionHandler(); +``` + +**File placement:** Always place exception handler classes in a `Middleware/` +folder to maintain consistent project organization. Do not place them at the +project root. + +### Step 6: Use a service layer + +Do not inject data stores directly into controllers or endpoint handlers. +Create a service interface and a sealed implementation class that owns the +data access logic and mapping between entities and request/response types. + +Always define an interface for every service — this enables unit testing with +mocks and follows the Dependency Inversion Principle: + +```csharp +// Services/IProductService.cs +public interface IProductService +{ + Task> GetAllAsync(CancellationToken ct); + Task GetByIdAsync(int id, CancellationToken ct); + Task CreateAsync(CreateProductRequest request, CancellationToken ct); +} + +// Services/ProductService.cs +public sealed class ProductService(...) : IProductService +{ + // Data access logic, entity-to-DTO mapping +} +``` + +Register with the interface, not the concrete type: + +```csharp +// In Program.cs +builder.Services.AddScoped(); +``` + +For EF Core data access patterns (migrations, Fluent API configuration, +`AsNoTracking`, seed data), see the `optimizing-ef-core-queries` skill. + +### Step 7: Create a .http test file + +After implementing endpoints, create a `.http` file in the project root that +demonstrates how to call every new endpoint. This serves as living +documentation and a quick manual test harness. + +```http +@baseUrl = http://localhost:5000 + +### Get all products +GET {{baseUrl}}/api/products + +### Get product by ID +GET {{baseUrl}}/api/products/1 + +### Create a product +POST {{baseUrl}}/api/products +Content-Type: application/json + +{ + "name": "Wireless Mouse", + "price": 29.99, + "category": "Electronics" +} + +### Delete a product +DELETE {{baseUrl}}/api/products/1 +``` + +Include at least one request per endpoint with realistic bodies. Show error +paths (e.g., non-existent IDs). Match the port to `launchSettings.json`. + +### Step 8: Build and verify + +1. Run `dotnet build` — confirm zero errors and zero warnings. +2. Start the app and verify the OpenAPI document loads (default: `/openapi/v1.json`). +3. Run the requests in the `.http` file and confirm correct status codes. + +## Validation + +- [ ] All endpoints return correct HTTP status codes per the table in Step 3 +- [ ] POST endpoints return `201 Created` with a `Location` header +- [ ] DELETE endpoints return `204 No Content` +- [ ] Every endpoint signature includes `CancellationToken` +- [ ] `CancellationToken` is forwarded to all downstream async calls +- [ ] OpenAPI document is generated and includes all new endpoints +- [ ] Endpoints have summary/description metadata for OpenAPI +- [ ] Enum values appear as strings in JSON responses and OpenAPI schemas (unless user explicitly requested integer serialization) +- [ ] Error responses use RFC 7807 Problem Details format +- [ ] Domain entities are not exposed directly in API request/response bodies +- [ ] All API-exposed DTOs have `` XML doc comments +- [ ] Date and time properties use `DateTimeOffset`, not `DateTime` +- [ ] A `.http` file exists with a request for every new endpoint +- [ ] `dotnet build` passes with zero errors and zero warnings +- [ ] All DTOs are `sealed record` types (not mutable classes) +- [ ] Minimal API handlers use `TypedResults` with explicit `Results` return types +- [ ] Every service has a corresponding interface registered in DI +- [ ] Exception handlers are placed in the `Middleware/` folder + +## Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Exposing domain entities as API responses | Create separate `sealed record` request/response types. Entities leak navigation properties and internal fields. | +| Forgetting `CancellationToken` | Add to every endpoint and forward through the entire async call chain. | +| Returning `200 OK` from POST create | Return `201 Created` with a `Location` header. | +| Missing OpenAPI metadata | Chain `.WithName()`, `.WithSummary()`, `.WithDescription()`, `.Produces()` on every endpoint. | +| Injecting data stores directly into endpoints | Use a service layer with an interface for separation and testability. | +| Mixing controller and minimal API styles | Pick one per project and be consistent. | +| `TypedResults` in ternary without explicit return type | `Ok` and `NotFound` have no common base — annotate with `Task, NotFound>>` or fall back to `Results` factory. | +| Using mutable classes for DTOs | Use `sealed record` with positional syntax (responses) or `init` properties (requests). | +| Registering services without interfaces | Define `IService` and register with `AddScoped()`. | +| Adding any `Swashbuckle.*` package to new .NET 9+ projects | Use built-in `AddOpenApi()` + `MapOpenApi()`. Do not add `Swashbuckle.AspNetCore`, `Swashbuckle.AspNetCore.SwaggerUI`, or any other Swashbuckle package. | +| Missing XML doc comments on DTOs | Add `` XML doc comments to every request and response type. These flow into the generated OpenAPI spec automatically. | +| Using `DateTime` for date/time properties | Use `DateTimeOffset` instead — it preserves UTC offset, avoids timezone ambiguity, and serializes correctly in JSON. | +| Serializing enums as integers | Configure `JsonStringEnumConverter` so enums serialize as strings by default. Only use integer serialization if the user explicitly requests it. | + +## More Info + +- [ASP.NET Core Web API overview](https://learn.microsoft.com/en-us/aspnet/core/web-api/) — fundamental concepts for building Web APIs +- [OpenAPI in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview) — built-in OpenAPI support in .NET 9+ +- [OpenAPI from XML comments](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments) — how XML doc comments flow into the OpenAPI spec +- [Minimal APIs overview](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) — routing, parameter binding, and response types +- [Handle errors in ASP.NET Core APIs](https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors) — Problem Details and exception handling +- [DateTimeOffset](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset) — preferred type for date/time values in APIs diff --git a/catalog/Frameworks/Official-DotNet-ASPNet/skills/dotnet-webapi/manifest.json b/catalog/Frameworks/Official-DotNet-ASPNet/skills/dotnet-webapi/manifest.json new file mode 100644 index 0000000..65c9d92 --- /dev/null +++ b/catalog/Frameworks/Official-DotNet-ASPNet/skills/dotnet-webapi/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Web", + "compatibility": "Requires an ASP.NET Core project or solution." +} diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-assertion-quality/manifest.json b/catalog/Platform/Official-DotNet-Experimental/skills/exp-assertion-quality/manifest.json deleted file mode 100644 index 5fff1ba..0000000 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-assertion-quality/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "0.1.0", - "category": "Testing", - "compatibility": "Requires a .NET repository where experimental upstream dotnet/skills guidance is acceptable." -} diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-dotnet-test-frameworks/SKILL.md b/catalog/Platform/Official-DotNet-Experimental/skills/exp-dotnet-test-frameworks/SKILL.md deleted file mode 100644 index fe4b2dd..0000000 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-dotnet-test-frameworks/SKILL.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -name: exp-dotnet-test-frameworks -description: "Reference data for .NET test framework detection patterns, assertion APIs, skip annotations, setup/teardown methods, and common test smell indicators across MSTest, xUnit, NUnit, and TUnit. Loaded by test analysis skills (exp-test-smell-detection, exp-assertion-quality, exp-test-maintainability, exp-test-tagging) as framework-specific lookup tables." -user-invocable: false -license: MIT ---- - -# .NET Test Framework Reference - -Language-specific detection patterns for .NET test frameworks (MSTest, xUnit, NUnit, TUnit). - -## Test File Identification - -| Framework | Test class markers | Test method markers | -| --------- | ------------------ | ------------------- | -| MSTest | `[TestClass]` | `[TestMethod]`, `[DataTestMethod]` | -| xUnit | _(none — convention-based)_ | `[Fact]`, `[Theory]` | -| NUnit | `[TestFixture]` | `[Test]`, `[TestCase]`, `[TestCaseSource]` | -| TUnit | `[ClassDataSource]` | `[Test]` | - -## Assertion APIs by Framework - -| Category | MSTest | xUnit | NUnit | -| -------- | ------ | ----- | ----- | -| Equality | `Assert.AreEqual` | `Assert.Equal` | `Assert.That(x, Is.EqualTo(y))` | -| Boolean | `Assert.IsTrue` / `Assert.IsFalse` | `Assert.True` / `Assert.False` | `Assert.That(x, Is.True)` | -| Null | `Assert.IsNull` / `Assert.IsNotNull` | `Assert.Null` / `Assert.NotNull` | `Assert.That(x, Is.Null)` | -| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` | -| Collection | `CollectionAssert.Contains` | `Assert.Contains` | `Assert.That(col, Has.Member(x))` | -| String | `StringAssert.Contains` | `Assert.Contains(str, sub)` | `Assert.That(str, Does.Contain(sub))` | -| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | -| Inconclusive | `Assert.Inconclusive()` | _skip via `[Fact(Skip)]`_ | `Assert.Inconclusive()` | -| Fail | `Assert.Fail()` | `Assert.Fail()` (.NET 10+) | `Assert.Fail()` | - -Third-party assertion libraries: `Should*` (Shouldly), `.Should()` (FluentAssertions / AwesomeAssertions), `Verify()` (Verify). - -## Sleep/Delay Patterns - -| Pattern | Example | -| ------- | ------- | -| Thread sleep | `Thread.Sleep(2000)` | -| Task delay | `await Task.Delay(1000)` | -| SpinWait | `SpinWait.SpinUntil(() => condition, timeout)` | - -## Skip/Ignore Annotations - -| Framework | Annotation | With reason | -| --------- | ---------- | ----------- | -| MSTest | `[Ignore]` | `[Ignore("reason")]` | -| xUnit | `[Fact(Skip = "reason")]` | _(reason is required)_ | -| NUnit | `[Ignore("reason")]` | _(reason is required)_ | -| TUnit | `[Skip("reason")]` | _(reason is required)_ | -| Conditional | `#if false` / `#if NEVER` | _(no reason possible)_ | - -## Exception Handling — Idiomatic Alternatives - -When a test uses `try`/`catch` to verify exceptions, suggest the framework-native alternative: - -**MSTest:** - -```csharp -// Instead of try/catch (matches exact type): -var ex = Assert.ThrowsExactly( - () => processor.ProcessOrder(emptyOrder)); -Assert.AreEqual("Order must contain at least one item", ex.Message); - -// Or (also matches derived types): -var ex = Assert.Throws( - () => processor.ProcessOrder(emptyOrder)); -Assert.AreEqual("Order must contain at least one item", ex.Message); -``` - -**xUnit:** - -```csharp -var ex = Assert.Throws( - () => processor.ProcessOrder(emptyOrder)); -Assert.Equal("Order must contain at least one item", ex.Message); -``` - -**NUnit:** - -```csharp -var ex = Assert.Throws( - () => processor.ProcessOrder(emptyOrder)); -Assert.That(ex.Message, Is.EqualTo("Order must contain at least one item")); -``` - -## Mystery Guest — Common .NET Patterns - -| Smell indicator | What to look for | -| --------------- | ---------------- | -| File system | `File.ReadAllText`, `File.Exists`, `File.WriteAllBytes`, `Directory.GetFiles`, `Path.Combine` with hard-coded paths | -| Database | `SqlConnection`, `DbContext` (without in-memory provider), `SqlCommand` | -| Network | `HttpClient` without `HttpMessageHandler` override, `WebRequest`, `TcpClient` | -| Environment | `Environment.GetEnvironmentVariable`, `Environment.CurrentDirectory` | -| Acceptable | `MemoryStream`, `StringReader`, `InMemory` database providers, custom `DelegatingHandler` | - -## Integration Test Markers - -Recognize these as integration tests (adjust smell severity accordingly): - -- Class name contains `Integration`, `E2E`, `EndToEnd`, or `Acceptance` -- `[TestCategory("Integration")]` (MSTest) -- `[Trait("Category", "Integration")]` (xUnit) -- `[Category("Integration")]` (NUnit) -- Project name ending in `.IntegrationTests` or `.E2ETests` - -## Setup/Teardown Methods - -| Framework | Setup | Teardown | -| --------- | ----- | -------- | -| MSTest | `[TestInitialize]` or constructor | `[TestCleanup]` or `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` | -| xUnit | constructor | `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` | -| NUnit | `[SetUp]` | `[TearDown]` | -| MSTest (class) | `[ClassInitialize]` | `[ClassCleanup]` | -| NUnit (class) | `[OneTimeSetUp]` | `[OneTimeTearDown]` | -| xUnit (class) | `IClassFixture` | fixture's `Dispose` | diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-dotnet-test-frameworks/manifest.json b/catalog/Platform/Official-DotNet-Experimental/skills/exp-dotnet-test-frameworks/manifest.json deleted file mode 100644 index 5fff1ba..0000000 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-dotnet-test-frameworks/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "0.1.0", - "category": "Testing", - "compatibility": "Requires a .NET repository where experimental upstream dotnet/skills guidance is acceptable." -} diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-gap-analysis/manifest.json b/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-gap-analysis/manifest.json deleted file mode 100644 index 5fff1ba..0000000 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-gap-analysis/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "0.1.0", - "category": "Testing", - "compatibility": "Requires a .NET repository where experimental upstream dotnet/skills guidance is acceptable." -} diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-maintainability/SKILL.md b/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-maintainability/SKILL.md index 4a6352e..64d780b 100644 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-maintainability/SKILL.md +++ b/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-maintainability/SKILL.md @@ -36,7 +36,7 @@ Analyze .NET test code for maintainability issues: duplicated boilerplate, copy- ### Step 1: Gather the test code -Read all test files the user provides or references. If the user points to a directory or project, scan for all test files — see the `exp-dotnet-test-frameworks` skill for framework-specific markers. +Read all test files the user provides or references. If the user points to a directory or project, scan for all test files — see the `dotnet-test-frameworks` skill for framework-specific markers. ### Step 2: Identify maintainability issues diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/manifest.json b/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/manifest.json deleted file mode 100644 index 5fff1ba..0000000 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "0.1.0", - "category": "Testing", - "compatibility": "Requires a .NET repository where experimental upstream dotnet/skills guidance is acceptable." -} diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-tagging/manifest.json b/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-tagging/manifest.json deleted file mode 100644 index 5fff1ba..0000000 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-tagging/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "0.1.0", - "category": "Testing", - "compatibility": "Requires a .NET repository where experimental upstream dotnet/skills guidance is acceptable." -} diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md index 2844e88..5660c48 100644 --- a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md @@ -11,7 +11,7 @@ description: > Dockerfiles for .NET 11. DO NOT USE FOR: .NET Framework migrations, upgrading from .NET 9 or earlier, greenfield .NET 11 projects, or cosmetic modernization unrelated to the upgrade. - NOTE: .NET 11 is in preview. Covers breaking changes through Preview 1. + NOTE: .NET 11 is in preview. Covers breaking changes through Preview 3. license: MIT --- @@ -19,7 +19,7 @@ license: MIT Migrate a .NET 10 project or solution to .NET 11, systematically resolving all breaking changes. The outcome is a project targeting `net11.0` that builds cleanly, passes tests, and accounts for every behavioral, source-incompatible, and binary-incompatible change introduced in .NET 11. -> **Note:** .NET 11 is currently in preview. This skill covers breaking changes documented through Preview 1. It will be updated as additional previews ship. +> **Note:** .NET 11 is currently in preview. This skill covers breaking changes documented through Preview 3. ## When to Use @@ -59,10 +59,14 @@ Migrate a .NET 10 project or solution to .NET 11, systematically resolving all b - **SDK attribute**: `Microsoft.NET.Sdk.Web` → ASP.NET Core; `Microsoft.NET.Sdk.WindowsDesktop` with `` or `` → WPF/WinForms - **PackageReferences**: `Microsoft.EntityFrameworkCore.*` → EF Core; `Microsoft.EntityFrameworkCore.Cosmos` → Cosmos DB provider - **Dockerfile presence** → Container changes relevant - - **Cryptography API usage** → DSA on macOS affected + - **Cryptography API usage** → DSA on macOS affected; AIA cert download changes relevant - **Compression API usage** → DeflateStream/GZipStream/ZipArchive changes relevant - - **TAR API usage** → Header checksum validation change relevant + - **TAR API usage** → Header checksum validation and HardLink entry changes relevant - **`NamedPipeClientStream` usage with `SafePipeHandle`** → SYSLIB0063 constructor obsoletion relevant + - **`BackgroundService` usage** → Unhandled exceptions now stop the host + - **`Microsoft.OpenApi` direct usage** → v3 API breaking changes in ASP.NET Core OpenAPI + - **EF Core SQL Server with Entra ID auth** → SqlClient 7.0 auth dependency changes + - **NativeAOT native libraries on Unix** → Output filename prefix changed 4. Record which reference documents are relevant (see the reference loading table in Step 3). 5. Do a **clean build** (`dotnet build --no-incremental` or delete `bin`/`obj`) on the current `net10.0` target to establish a clean baseline. Record any pre-existing warnings. @@ -93,9 +97,10 @@ Load reference documents based on the project's technology areas: | `references/csharp-compiler-dotnet10to11.md` | Always (C# 15 compiler breaking changes) | | `references/core-libraries-dotnet10to11.md` | Always (applies to all .NET 11 projects) | | `references/sdk-msbuild-dotnet10to11.md` | Always (SDK and build tooling changes) | -| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core (especially Cosmos DB provider) | -| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs or targets macOS | -| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware or embedded devices | +| `references/aspnetcore-dotnet10to11.md` | Project uses ASP.NET Core (OpenAPI, Blazor) | +| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core | +| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs, mTLS, or targets macOS | +| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware, embedded devices, or using NativeAOT | Work through each build error systematically. Common patterns: @@ -115,6 +120,12 @@ Work through each build error systematically. Common patterns: 8. **`when` switch-expression-arm parsing** — `(X.Y) when` is now parsed as a constant pattern with a `when` clause instead of a cast expression, which can cause existing code to fail to compile or change meaning. Review switch expressions using `when` and adjust syntax as needed. +9. **Microsoft.OpenApi v3 breaking changes** — `Microsoft.AspNetCore.OpenApi` now depends on `Microsoft.OpenApi` 3.x. Code using `Microsoft.OpenApi` types directly (`OpenApiDocument`, `OpenApiSchema`, etc.) will have compile errors. Follow the v3 upgrade guide. + +10. **EF Core Design package no longer transitive** — `Microsoft.EntityFrameworkCore.Tools` and `.Tasks` no longer depend on `.Design`. Add an explicit `PackageReference` if needed. + +11. **EFOptimizeContext MSBuild property removed** — Replace with `` and ``. + ### Step 4: Address behavioral changes These changes compile successfully but alter runtime behavior. Review each one and determine impact: @@ -137,6 +148,24 @@ These changes compile successfully but alter runtime behavior. Review each one a 9. **Mono launch target for .NET Framework** — No longer set automatically. If using Mono for .NET Framework apps on Linux, specify explicitly. +10. **Unhandled BackgroundService exceptions stop the host** — Exceptions from `ExecuteAsync()` now propagate and crash the host. Add try/catch in background services that should not bring down the application. + +11. **ZipArchive CRC32 validation** — ZIP reads now validate CRC32 checksums. Corrupt or truncated archives that previously succeeded will now throw `InvalidDataException`. + +12. **TarWriter emits HardLink entries** — Hard-linked files are now written as `HardLink` entries instead of duplicated data. Consumers of .NET-produced tar archives must handle `HardLink` entries. + +13. **AIA certificate downloads disabled** — Server-side client-certificate validation no longer downloads intermediate CAs via AIA by default. Pre-install the full chain or have clients send intermediates. + +14. **Blazor Virtualize OverscanCount default changed** — Default `OverscanCount` changed from 3 to 15. Set explicitly if performance-sensitive. + +15. **Microsoft.Data.SqlClient 7.0 — Entra ID auth separated** — Azure/Entra ID authentication dependencies removed from the core SqlClient package. Add `Microsoft.Data.SqlClient.Extensions.Azure` if using Entra ID auth. + +16. **SqlVector<T> excluded from SELECT** — Vector properties are no longer auto-loaded. Use explicit projections to include vector values. + +17. **SQLitePCLRaw encryption bundles removed** — `bundle_e_sqlcipher` and other encryption bundle packages removed in SQLitePCLRaw 3.0. + +18. **NativeAOT Unix native library `lib` prefix** — Output filenames now include `lib` prefix on Linux/macOS (e.g., `libMyLib.so`). + ### Step 5: Update infrastructure 1. **Dockerfiles**: Update base images from 10.0 to 11.0: @@ -155,7 +184,7 @@ These changes compile successfully but alter runtime behavior. Review each one a "sdk": { - "version": "10.0.100", - "rollForward": "latestFeature" - + "version": "11.0.100-preview.1", + + "version": "11.0.100-preview.3", + "rollForward": "latestFeature" }, "otherSettings": { @@ -173,11 +202,15 @@ These changes compile successfully but alter runtime behavior. Review each one a 3. If the application is containerized, build and test the container image 4. Smoke-test the application, paying special attention to: - Compression behavior with empty streams - - TAR file reading + - TAR file reading (checksum validation and HardLink entries) - EF Core Cosmos DB operations (must be async) - DSA usage on macOS - Memory-intensive MemoryStream usage - Span collection expression assignments + - BackgroundService exception handling + - mTLS / client certificate chain validation + - EF Core SQL Server with Entra ID authentication + - NativeAOT output filenames on Unix 5. Review the diff and ensure no unintended behavioral changes were introduced ## Reference Documents @@ -189,6 +222,7 @@ The `references/` folder contains detailed breaking change information organized | `references/csharp-compiler-dotnet10to11.md` | Always (C# 15 compiler breaking changes) | | `references/core-libraries-dotnet10to11.md` | Always (applies to all .NET 11 projects) | | `references/sdk-msbuild-dotnet10to11.md` | Always (SDK and build tooling changes) | -| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core (especially Cosmos DB provider) | -| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs or targets macOS | -| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware or embedded devices | +| `references/aspnetcore-dotnet10to11.md` | Project uses ASP.NET Core (OpenAPI, Blazor) | +| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core | +| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs, mTLS, or targets macOS | +| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware, embedded devices, or using NativeAOT | diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/aspnetcore-dotnet10to11.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/aspnetcore-dotnet10to11.md new file mode 100644 index 0000000..bc97680 --- /dev/null +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/aspnetcore-dotnet10to11.md @@ -0,0 +1,27 @@ +# ASP.NET Core Breaking Changes (.NET 11) + +These breaking changes affect ASP.NET Core projects. Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/11 + +> **Note:** .NET 11 is in preview. Additional ASP.NET Core breaking changes are expected in later previews. + +## Source-Incompatible Changes + +### Microsoft.OpenApi updated to v3 with OpenAPI 3.2.0 support (Preview 2) + +**Impact: Medium.** `Microsoft.AspNetCore.OpenApi` updated its dependency from `Microsoft.OpenApi` 2.x to 3.x, adding OpenAPI 3.2.0 document generation. The underlying `Microsoft.OpenApi` library has breaking API changes in the v2→v3 transition. + +Code that directly uses `Microsoft.OpenApi` types (`OpenApiDocument`, `OpenApiSchema`, `OpenApiOperation`, etc.) will have compile errors. + +**Fix:** Follow the [Microsoft.OpenApi v3 upgrade guide](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-3.md). If you only use the ASP.NET Core OpenAPI integration (`.WithOpenApi()`, `MapOpenApi()`) without touching the object model directly, no changes are needed. + +Source: https://github.com/dotnet/aspnetcore/pull/65415 + +## Behavioral Changes + +### Blazor Virtualize<T> default OverscanCount changed from 3 to 15 (Preview 3) + +**Impact: Low.** The default `OverscanCount` on the `Virtualize` component changed from `3` to `15` to support variable-height item measurement. `QuickGrid` retains its own default of `3`. + +**Fix:** If performance-sensitive, set `OverscanCount` explicitly: ``. + +Source: https://github.com/dotnet/aspnetcore/pull/64964 diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md index d5a7122..ad45fbb 100644 --- a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md @@ -85,3 +85,57 @@ Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-librari **Impact: Low.** The minimum supported date for the Japanese Calendar has been corrected. Code using very early dates in the Japanese Calendar may be affected. Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/globalization/11/japanese-calendar-min-date + +### ZipArchive now validates CRC32 when reading entries (Preview 3) + +**Impact: Low–Medium.** ZIP archive reads now validate the CRC32 checksum of each entry. Previously, corrupt or truncated archives were silently accepted; they now throw `InvalidDataException`. + +**Fix:** Ensure ZIP files are not corrupted. If processing partially-written or legacy archives, add error handling for `InvalidDataException`. + +Source: https://github.com/dotnet/runtime/pull/124766 + +### Unhandled BackgroundService exceptions now stop the host (Preview 3) + +**Impact: Medium.** Unhandled exceptions thrown from `BackgroundService.ExecuteAsync()` now propagate and stop the host application. Previously they were silently swallowed. + +```csharp +// .NET 10: exception silently swallowed, host continues +// .NET 11: exception propagates, host stops +protected override async Task ExecuteAsync(CancellationToken stoppingToken) +{ + throw new InvalidOperationException("oops"); // now kills the host +} + +// FIX: Add proper exception handling +protected override async Task ExecuteAsync(CancellationToken stoppingToken) +{ + try + { + // ... work ... + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogError(ex, "Background service failed"); + } +} +``` + +**Fix:** Add try/catch in `ExecuteAsync()` for any `BackgroundService` that should not crash the host on failure. + +Source: https://github.com/dotnet/runtime/pull/124863 + +### TarWriter emits HardLink entries for hard-linked files (Preview 3) + +**Impact: Low.** When `TarWriter` archives a directory containing hard links, the same inode encountered more than once is now written as a `HardLink` entry pointing back to the first occurrence, rather than duplicating the file data. + +**Fix:** If consuming tar archives produced by .NET code, ensure the reader handles `HardLink` entry types. + +Source: https://github.com/dotnet/runtime/pull/123874 + +### Zstandard APIs moved from preview package to System.IO.Compression (Preview 3) + +**Impact: Low.** `ZstandardStream` and related APIs that were previously in the `System.IO.Compression.Zstandard` preview NuGet package are now in-box in `System.IO.Compression`. + +**Fix:** Remove the `` preview package if present. The APIs are now available without any additional package reference. + +Source: https://github.com/dotnet/runtime/pull/114545 diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md index 9f88243..6ed0516 100644 --- a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md @@ -26,3 +26,14 @@ var signature = ecdsa.SignData(data, HashAlgorithmName.SHA256); - **Ed25519** — if available in your scenario This change only affects macOS. DSA continues to work on Windows and Linux (though it is generally considered a legacy algorithm). + +### AIA certificate downloads disabled by default during client-certificate validation (Preview 3) + +**Impact: Medium.** AIA (Authority Information Access) certificate downloads are now disabled by default when performing server-side client-certificate chain validation. Previously the runtime would attempt to fetch intermediate CA certificates online. + +**Fix:** If using mTLS where client certificates rely on AIA URLs for intermediate CAs, either: +- Pre-install the full certificate chain on the server +- Have clients send the full chain including intermediates +- Re-enable AIA downloads via `X509ChainPolicy.DisableCertificateDownloads = false` + +Source: https://github.com/dotnet/runtime/pull/125049 diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md index 4c417d2..d2e9cc5 100644 --- a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md @@ -2,7 +2,7 @@ These breaking changes affect projects using Entity Framework Core 11. Source: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-11.0/breaking-changes -> **Note:** .NET 11 is in preview. The changes below were introduced in **Preview 1**. Additional EF Core breaking changes are expected in later previews. +> **Note:** .NET 11 is in preview. The changes below were introduced in **Preview 1 through Preview 3**. Additional EF Core breaking changes are expected in later previews. ## Medium-Impact Changes @@ -36,3 +36,69 @@ await context.SaveChangesAsync(); - `Any()` → `await AnyAsync()` Tracking issue: https://github.com/dotnet/efcore/issues/37059 + +### Cosmos: empty owned collections return empty collection instead of null (Preview 1) + +**Impact: Low.** When a Cosmos-backed entity has an owned collection with no items, the property now returns an empty collection rather than `null`. + +**Fix:** Update null checks to empty-collection checks: `if (entity.Items is null)` → `if (entity.Items.Count == 0)`. + +Tracking issue: https://github.com/dotnet/efcore/issues/36577 + +## Preview 3 Changes + +### RelationalEventId.MigrationsNotFound now throws by default (Preview 3) + +**Impact: Low.** Calling `Migrate()` or `MigrateAsync()` when no migrations exist in the assembly now throws an exception rather than silently logging. + +**Fix:** If intentional, suppress with: `options.ConfigureWarnings(w => w.Ignore(RelationalEventId.MigrationsNotFound))`. + +Source: https://github.com/dotnet/efcore/pull/37839 + +### EF Core Tools and Tasks no longer transitively depend on Design (Preview 3) + +**Impact: Low.** The `Microsoft.EntityFrameworkCore.Tools` and `Microsoft.EntityFrameworkCore.Tasks` NuGet packages no longer have a transitive dependency on `Microsoft.EntityFrameworkCore.Design`. + +**Fix:** If your project relied on this transitive reference, add it explicitly: + +```xml + +``` + +Source: https://github.com/dotnet/efcore/pull/37837 + +### EFOptimizeContext MSBuild property removed (Preview 3) + +**Impact: Low.** The `true` MSBuild property no longer exists. Code generation is now controlled by `` and ``. + +**Fix:** Replace `` with the two new properties. With `PublishAOT=true`, generation is automatic during publish. + +Source: https://github.com/dotnet/efcore/pull/37838 + +### SqlVector<T> properties excluded from SELECT by default (Preview 3) + +**Impact: Low.** `SqlVector` properties are now excluded from `SELECT` statements when materializing entities (they return `null`). They can still be used in `WHERE`/`ORDER BY` for vector search. + +**Fix:** Use explicit projections to include vector values: `.Select(b => new { b.Id, b.Embedding })`. + +Source: https://github.com/dotnet/efcore/pull/37829 + +### Microsoft.Data.SqlClient updated to 7.0 (Preview 3) + +**Impact: Medium.** EF Core's SQL Server provider now depends on `Microsoft.Data.SqlClient` 7.0. In v7, Azure/Entra ID authentication dependencies (`Azure.Core`, `Azure.Identity`, `Microsoft.Identity.Client`) have been removed from the core package. + +**Fix:** If using Entra ID authentication (e.g., `ActiveDirectoryDefault`, `ActiveDirectoryManagedIdentity`), add: + +```xml + +``` + +Source: https://github.com/dotnet/efcore/pull/37949 + +### Encryption-enabled SQLite packages removed (Preview 3) + +**Impact: Medium.** `SQLitePCLRaw 3.0` (used by `Microsoft.Data.Sqlite` 11) removed `bundle_e_sqlcipher` and several other bundle packages. + +**Fix:** Switch to SQLite Encryption Extension (SEE), SQLCipher from Zetetic, or `SQLite3MultipleCiphers-NuGet`. + +Source: https://github.com/dotnet/efcore/issues/37059 diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md index a290592..b2ded5f 100644 --- a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md @@ -49,3 +49,11 @@ For ReadyToRun-capable assemblies, there may be additional startup overhead on s **Fix:** Verify all deployment targets meet the new minimum requirements. For x86/x64, any CPU from ~2013 or later should be fine. For Windows Arm64, ensure `LSE` support (all Windows 11 compatible Arm64 devices). Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/jit/11/minimum-hardware-requirements + +### NativeAOT native-library outputs use `lib` prefix on Unix (Preview 3) + +**Impact: Low.** NativeAOT shared/native library outputs on Linux and macOS now follow Unix conventions and include the `lib` prefix (e.g., `libMyLib.so` instead of `MyLib.so`). + +**Fix:** Update build scripts, deployment pipelines, or P/Invoke declarations that reference output filenames by the old name without the `lib` prefix. + +Source: https://github.com/dotnet/runtime/pull/124611 diff --git a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md index 16377e7..e256ea6 100644 --- a/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md +++ b/catalog/Platform/Official-DotNet-Upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md @@ -11,3 +11,19 @@ These changes affect the .NET SDK, CLI tooling, NuGet, and MSBuild behavior. Sou **Impact: Low.** The mono launch target is no longer set automatically for .NET Framework apps. If you require Mono for execution on Linux, you need to specify it explicitly in the configuration. Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/11/mono-launch-target-removed + +### NETSDK1235 warning for PackAsTool with custom .nuspec (Preview 2) + +**Impact: Low.** A new build warning `NETSDK1235` is emitted when a project has both `PackAsTool=true` and a custom `NuspecFile` property, which violates .NET Tool packaging requirements. Projects with `TreatWarningsAsErrors=true` will fail. + +**Fix:** Remove the custom `NuspecFile` property when packaging as a .NET Tool, or suppress the warning if the .nuspec is compatible. + +Source: https://github.com/dotnet/sdk/pull/52810 + +### `dotnet publish --self-contained` now parses the passed value (Preview 3) + +**Impact: Low.** `dotnet publish --self-contained` previously always interpreted the flag as `true` regardless of the passed value. It now correctly parses the value (e.g., `--self-contained false` actually produces a framework-dependent publish). + +**Fix:** Review build scripts that pass `--self-contained` to ensure the intended value is correct. + +Source: https://github.com/dotnet/sdk/pull/52333 diff --git a/catalog/Platform/Official-DotNet/skills/csharp-scripts/SKILL.md b/catalog/Platform/Official-DotNet/skills/csharp-scripts/SKILL.md index dad39fe..8f5c41b 100644 --- a/catalog/Platform/Official-DotNet/skills/csharp-scripts/SKILL.md +++ b/catalog/Platform/Official-DotNet/skills/csharp-scripts/SKILL.md @@ -1,41 +1,44 @@ --- name: csharp-scripts -description: Run single-file C# programs as scripts (file-based apps) for quick experimentation, prototyping, and concept testing. Use when the user wants to write and execute a small C# program without creating a full project. +description: "Run file-based C# apps with the .NET CLI when the user explicitly wants C#/.NET code without creating a project. Use for C# language/API experiments, one-file C# apps, small multi-file C# apps composed with `#:include`/`#:exclude`, or C# file-based apps linked with `#:ref`. Do not use for language-agnostic throwaway scripts, generic computations, Python/PowerShell-style automation, full projects, or existing app integration." license: MIT --- -# C# Scripts +# File-Based C# Apps ## When to Use -- Testing a C# concept, API, or language feature with a quick one-file program +- Testing a C# concept, API, or language feature with a quick file-based app - Prototyping logic before integrating it into a larger project +- Building a small utility from one entry-point file and a few helper `.cs` files ## When Not to Use -- The user needs a full project with multiple files or project references +- The user asks for a language-agnostic quick script, throwaway computation, or shell/Python/PowerShell-style automation +- The user needs a full project, solution integration, or project references in an existing app - The user is working inside an existing .NET solution and wants to add code there -- The program is too large or complex for a single file +- The app is large enough that project structure, build customization, tests, or publish configuration should live in a `.csproj` ## Inputs | Input | Required | Description | |-------|----------|-------------| -| C# code or intent | Yes | The code to run, or a description of what the script should do | +| C# code or intent | Yes | The code to run, or a description of what the file-based app should do | ## Workflow ### Step 1: Check the .NET SDK version -Run `dotnet --version` to verify the SDK is installed and note the major version number. File-based apps require .NET 10 or later. If the version is below 10, follow the [fallback for older SDKs](#fallback-for-net-9-and-earlier) instead. +Run `dotnet --version` to verify the SDK is installed and note the full version, including the feature band. File-based apps require .NET 10 or later. `#:include`, `#:exclude`, and transitive directive processing require SDK 10.0.300 or later; SDK 10.0.100/10.0.200 builds can run single-file apps but do not support those multi-file directives. If the version is below 10, follow the [fallback for older SDKs](#fallback-for-net-9-and-earlier) instead. -### Step 2: Write the script file +### Step 2: Write the app file -Create a single `.cs` file using top-level statements. Place it outside any existing project directory to avoid conflicts with `.csproj` files. +Create an entry-point `.cs` file using top-level statements. Place it outside any existing project directory to avoid conflicts with `.csproj` files. ```csharp +#!/usr/bin/env dotnet // hello.cs -Console.WriteLine("Hello from a C# script!"); +Console.WriteLine("Hello from a file-based app!"); var numbers = new[] { 1, 2, 3, 4, 5 }; Console.WriteLine($"Sum: {numbers.Sum()}"); @@ -47,7 +50,7 @@ Guidelines: - Place `using` directives at the top of the file (after the `#!` line and any `#:` directives if present) - Place type declarations (classes, records, enums) after all top-level statements -### Step 3: Run the script +### Step 3: Run the app ```bash dotnet hello.cs @@ -65,7 +68,7 @@ Place directives at the top of the file (immediately after an optional shebang l #### `#:package` — NuGet package references -Always specify a version: +Specify a version unless the app intentionally uses central package management. Use `@*` when the latest available package is acceptable (or `@*-*` for pre-release): ```csharp #:package Humanizer@2.14.1 @@ -109,6 +112,26 @@ Reference another project by relative path: #:project ../MyLibrary/MyLibrary.csproj ``` +#### `#:ref` — File-based app references + +Reference another `.cs` file as a separate file-based app project when it should compile into a separate assembly instead of being included in the same compilation. Use `#:include` for ordinary helper files that should share the same assembly as the entry point; use `#:ref` when you want project-reference-like boundaries. + +```csharp +#:property ExperimentalFileBasedProgramEnableRefDirective=true +#:ref ../Shared/Formatter.cs + +Console.WriteLine(Formatter.Title("hello world")); +``` + +Guidelines: + +- The referenced file is compiled as its own virtual project and added as a project reference. +- If the referenced file is a library without top-level statements, put `#:property OutputType=Library` in that referenced file. +- Members that must be consumed by the referencing app should be public; internal members are not visible across the assembly boundary. +- `#:ref` is transitive: a referenced file can contain its own `#:ref` and other `#:` directives. +- Relative paths are resolved relative to the file containing the directive. +- Some SDK builds require `#:property ExperimentalFileBasedProgramEnableRefDirective=true`; remove that property if the SDK accepts `#:ref` without it. + #### `#:sdk` — SDK selection Override the default SDK (`Microsoft.NET.Sdk`): @@ -117,9 +140,65 @@ Override the default SDK (`Microsoft.NET.Sdk`): #:sdk Microsoft.NET.Sdk.Web ``` +#### `#:include` and `#:exclude` — Multi-file apps + +In .NET SDK 10.0.300 and later, file-based apps can include additional files in the same virtual project. Check the full `dotnet --version` output before using these directives; a 10.0.100 or 10.0.200 SDK is still .NET 10 but does not support them. Use `#:include` for helper source files and supported assets, and `#:exclude` to remove files from an include pattern or default item set. + +```csharp +#!/usr/bin/env dotnet +#:include Helpers.cs +#:include Models/*.cs +#:exclude Models/Generated/*.cs + +Console.WriteLine(Formatter.Title("hello world")); +``` + +Guidelines: + +- Treat the file passed to `dotnet` as the entry point; put top-level statements there. +- Put declarations such as classes, records, and enums in included `.cs` files. +- Prefer explicit globs such as `Helpers.cs` or `Models/*.cs` over broad recursive globs. +- Paths are resolved relative to the file containing the directive. +- Include directives from non-entry-point C# files are processed too, so a helper file can declare its own `#:package`, `#:property`, `#:sdk`, `#:project`, `#:ref`, `#:include`, or `#:exclude` directives. +- Avoid duplicate directives across included files unless the directive kind explicitly supports duplicates; duplicate `#:package`, `#:property`, `#:sdk`, `#:include`, and `#:exclude` entries can fail. +- When an app uses `#:include`, add a shebang (`#!/usr/bin/env dotnet`) to the entry-point file on Unix-like systems to make the entry point clear to tools. Use `LF` line endings and no BOM for shebang files. + +Example layout: + +```text +scratch/ + hello.cs + Helpers.cs + Models/ + Person.cs +``` + +```csharp +#!/usr/bin/env dotnet +// hello.cs +#:include Helpers.cs +#:include Models/*.cs + +var person = new Person("Ada"); +Console.WriteLine(Formatter.Title(person.Name)); +``` + +```csharp +// Helpers.cs +static class Formatter +{ + public static string Title(string value) => value.ToUpperInvariant(); +} +``` + +```csharp +// Models/Person.cs +record Person(string Name); +``` + ### Step 5: Clean up -Remove the script file when the user is done. To clear cached build artifacts: +Remove the app files when the user is done. To clear cached build artifacts: ```bash dotnet clean hello.cs @@ -173,7 +252,7 @@ partial class AppJsonContext : JsonSerializerContext; ## Converting to a project -When a script outgrows a single file, convert it to a full project: +When a file-based app outgrows this workflow, convert it to a full project: ```bash dotnet project convert hello.cs @@ -184,29 +263,35 @@ dotnet project convert hello.cs If the .NET SDK version is below 10, file-based apps are not available. Use a temporary console project instead: ```bash -mkdir -p /tmp/csharp-script && cd /tmp/csharp-script +mkdir -p /tmp/csharp-file-based-app && cd /tmp/csharp-file-based-app dotnet new console -o . --force ``` -Replace the generated `Program.cs` with the script content and run with `dotnet run`. Add NuGet packages with `dotnet add package `. Remove the directory when done. +Replace the generated `Program.cs` with the app content and run with `dotnet run`. Add NuGet packages with `dotnet add package `. Remove the directory when done. ## Validation - [ ] `dotnet --version` reports 10.0 or later (or fallback path is used) -- [ ] The script compiles without errors (can be checked explicitly with `dotnet build .cs`) +- [ ] If the app uses `#:include`, `#:exclude`, or transitive directives from included files, `dotnet --version` reports SDK 10.0.300 or later +- [ ] The app compiles without errors (can be checked explicitly with `dotnet build .cs`) - [ ] `dotnet .cs` produces the expected output -- [ ] Script file and cached artifacts are cleaned up after the session +- [ ] Multi-file apps include every required helper file and exclude unintended matches +- [ ] App files and cached artifacts are cleaned up after the session ## Common Pitfalls | Pitfall | Solution | |---------|----------| -| `.cs` file is inside a directory with a `.csproj` | Move the script outside the project directory, or use `dotnet run --file file.cs` | +| `.cs` file is inside a directory with a `.csproj` | Move the app outside the project directory, or use `dotnet run --file file.cs` | | `#:package` without a version | Specify a version: `#:package PackageName@1.2.3` or `@*` for latest | | `#:property` with wrong syntax | Use `PropertyName=Value` with no spaces around `=` and no quotes: `#:property AllowUnsafeBlocks=true` | | Directives placed after C# code | All `#:` directives must appear immediately after an optional shebang line (if present) and before any `using` directives or other C# statements | +| Helper file is not compiled | Add `#:include Helper.cs` or an appropriate glob to the entry-point file | +| Shared file needs an assembly boundary | Use `#:ref Shared.cs` instead of `#:include Shared.cs`, and set `#:property OutputType=Library` in the referenced file if it has no entry point | +| Broad include pulls in unrelated files | Prefer narrow include patterns and use `#:exclude` for generated, backup, or experimental files | +| Duplicate directives in included files | Keep package, property, SDK, include, and exclude directives unique across the entry point and included C# files | | Reflection-based JSON serialization fails | Use source-generated JSON with `JsonSerializerContext` (see [Source-generated JSON](#source-generated-json)) | -| Unexpected build behavior or version errors | File-based apps inherit `global.json`, `Directory.Build.props`, `Directory.Build.targets`, and `nuget.config` from parent directories. Move the script to an isolated directory if the inherited settings conflict | +| Unexpected build behavior or version errors | File-based apps inherit `global.json`, `Directory.Build.props`, `Directory.Build.targets`, and `nuget.config` from parent directories. Move the app to an isolated directory if the inherited settings conflict | ## More info diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md index ab4722a..1e08d22 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md @@ -2,9 +2,10 @@ description: >- Orchestrates comprehensive test generation using Research-Plan-Implement pipeline. Use when asked to generate tests, write unit - tests, improve test coverage, or add tests. + tests, improve test coverage, or add tests. DO NOT USE FOR: diagnosing + coverage plateaus or project-wide coverage/CRAP analysis without writing tests + (use coverage-analysis); targeted method/class CRAP scores (use crap-score). name: code-testing-generator -tools: ['read', 'search', 'edit', 'task', 'skill', 'terminal'] license: MIT --- @@ -34,7 +35,7 @@ Based on the request scope, pick exactly one strategy and follow it: | Strategy | When to use | What to do | | ---------- | ------------- | ------------ | -| **Direct** | A small, self-contained request (e.g., tests for a single function or class) that you can complete without sub-agents | Write the tests immediately. **Run them right away** — if any test fails, read the production code, fix the assertion, and re-run before writing more tests. Skip Steps 3-5 (research, plan, implement sub-agents). Then proceed to Steps 6-9 for validation and reporting. | +| **Direct** | A small, self-contained request (e.g., tests for a single function or class) that you can complete without sub-agents | Follow the codebase conventions on test file structure, naming, style, and testing approaches. Reuse existing test projects and test files when possible — if the code under test already has tests, add new tests to the same file or test project. Only create a new test file when no canonical file is named or discoverable for the symbol under test. Write the tests immediately. **Run them right away** — if any test fails, read the production code, fix the assertion, and re-run before writing more tests. Skip Steps 3-5 (research, plan, implement sub-agents). Then proceed to Steps 6-9 for validation and reporting. | | **Single pass** | A moderate scope (couple projects or modules) that a single Research → Plan → Implement cycle can cover | Execute Steps 3-8 once, then proceed to Step 9. | | **Iterative** | A large scope or ambitious coverage target that one pass cannot satisfy | Execute Steps 3-8, then re-evaluate coverage. If the target is not met, repeat Steps 3-8 with a narrowed focus on remaining gaps. Use unique names for each iteration's `.testagent/` documents (e.g., `research-2.md`, `plan-2.md`) so earlier results are not overwritten. Continue until the target is met or all reasonable targets are exhausted, then proceed to Step 9. | diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md index 85c8fcb..1099744 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md @@ -78,5 +78,5 @@ Failures: - Include file:line references when available - **For .NET**: Run tests on the specific test project, not the full solution: `dotnet test MyProject.Tests.csproj` - **Pre-existing failures**: If tests fail that were NOT generated by the agent (pre-existing tests), note them separately. Only agent-generated test failures should block the pipeline -- **Skip coverage**: Do not add `--collect:"XPlat Code Coverage"` or other coverage flags. Coverage collection is not the agent's responsibility +- **Skip coverage by default**: Do not add `--collect:"XPlat Code Coverage"` or other coverage flags to the test command — coverage collection is not the agent's responsibility. **Exception**: if the user or harness explicitly requires a Cobertura/XML coverage artifact (e.g., they ask for `coverlet.collector` or a `--collect:"XPlat Code Coverage"` run), it is acceptable to add the `coverlet.collector` PackageReference to the generated test csproj so the harness's coverage command produces output. Do not run the coverage command yourself; leave that to the validation step - **Failure analysis for generated tests**: When reporting failures in freshly generated tests, note that these tests have never passed before. The most likely cause is incorrect test expectations (wrong expected values, wrong mock setup), not production code bugs diff --git a/catalog/Testing/Official-DotNet-Test/agents/test-migration/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/test-migration/AGENT.md index d218c74..1b1a11c 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/test-migration/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/test-migration/AGENT.md @@ -6,7 +6,6 @@ description: >- and guides users through end-to-end upgrades. Use when asked to upgrade MSTest, migrate to xUnit v3, switch to Microsoft.Testing.Platform, modernize test infrastructure, or when the user says "migrate my tests". -tools: ['read', 'search', 'edit', 'terminal', 'skill'] user-invokable: true disable-model-invocation: false handoffs: diff --git a/catalog/Testing/Official-DotNet-Test/agents/test-quality-auditor/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/test-quality-auditor/AGENT.md index 0913620..9d5065b 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/test-quality-auditor/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/test-quality-auditor/AGENT.md @@ -9,7 +9,6 @@ description: >- running multiple analysis skills in sequence. Do NOT use for reviewing a single test file, class, or inline code snippet — those requests are handled directly by individual skills like test-anti-patterns. -tools: ['read', 'search', 'edit', 'terminal', 'skill'] user-invokable: true disable-model-invocation: false handoffs: @@ -60,13 +59,13 @@ Classify the user's request and route to the appropriate skill: | User Intent | Route To | Plugin | |---|---|---| -| "Are my assertions good enough?" / shallow testing / assertion diversity | `exp-assertion-quality` skill | dotnet-experimental | -| "Find test smells" / comprehensive formal audit | `exp-test-smell-detection` skill | dotnet-experimental | +| "Are my assertions good enough?" / shallow testing / assertion diversity | `assertion-quality` skill | dotnet-test | +| "Find test smells" / comprehensive formal audit | `test-smell-detection` skill | dotnet-test | | "Pragmatic anti-pattern check" within a broader audit context | `test-anti-patterns` skill | dotnet-test | | "Find test duplication" / boilerplate / DRY up tests | `exp-test-maintainability` skill | dotnet-experimental | | "Are my mocks needed?" / over-mocking / mock audit | `exp-mock-usage-analysis` skill | dotnet-experimental | -| "Would my tests catch bugs?" / mutation analysis / test gaps | `exp-test-gap-analysis` skill | dotnet-experimental | -| "Categorize my tests" / tag tests / trait distribution | `exp-test-tagging` skill | dotnet-experimental | +| "Would my tests catch bugs?" / mutation analysis / test gaps | `test-gap-analysis` skill | dotnet-test | +| "Categorize my tests" / tag tests / trait distribution | `test-tagging` skill | dotnet-test | | "Coverage report" / risk hotspots / CRAP score | `coverage-analysis` skill (use `crap-score` only for explicitly targeted method/class CRAP analysis or narrow-scope Cobertura data) | dotnet-test | | "Find untestable code" / static dependencies | `detect-static-dependencies` skill → hand off to `testability-migration` agent for fixes | dotnet-test | | "Full health check" / "audit my tests" / broad quality request | Run the **Comprehensive Audit Pipeline** below | multiple | @@ -83,11 +82,11 @@ Run these in order. Each step builds context for the next. Stop early if the use - Quick pragmatic scan for the most impactful issues - Produces severity-ranked findings (Critical → Low) -2. **Assertion quality** — `exp-assertion-quality` skill +2. **Assertion quality** — `assertion-quality` skill - Measures assertion variety and depth - Reveals whether tests actually verify meaningful behavior -3. **Test gaps** — `exp-test-gap-analysis` skill +3. **Test gaps** — `test-gap-analysis` skill - Pseudo-mutation analysis to find blind spots - Answers "would tests catch a bug here?" @@ -97,10 +96,10 @@ Run these in order. Each step builds context for the next. Stop early if the use ### Optional follow-ups (offer but don't run automatically) -5. **Test smells** — `exp-test-smell-detection` skill (if step 1 found many issues and the user wants a deeper formal audit) +5. **Test smells** — `test-smell-detection` skill (if step 1 found many issues and the user wants a deeper formal audit) 6. **Maintainability** — `exp-test-maintainability` skill (if the test suite is large and duplication is suspected) 7. **Mock audit** — `exp-mock-usage-analysis` skill (if over-mocking was flagged in step 1) -8. **Test tagging** — `exp-test-tagging` skill (if the user wants to understand test type distribution) +8. **Test tagging** — `test-tagging` skill (if the user wants to understand test type distribution) ### Synthesizing results @@ -152,4 +151,4 @@ Prioritize findings by impact: - **Always start with detection**: Identify test framework, test project paths, and approximate test count before diving into analysis - **Lead with actionable findings**: Put the most impactful issues first - **Distinguish analysis from action**: This agent produces reports. If the user wants to fix issues, point them to the appropriate skill or agent (e.g., `testability-migration` for static dependencies, `code-testing-generator` for writing new tests) -- **Be honest about experimental skills**: Skills from `dotnet-experimental` are being refined — mention this context when presenting their results +- **Be honest about experimental skills**: Skills from `dotnet-experimental` (`exp-test-maintainability`, `exp-mock-usage-analysis`) are being refined — mention this context when presenting their results diff --git a/catalog/Testing/Official-DotNet-Test/agents/testability-migration/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/testability-migration/AGENT.md index 87f75c9..f280226 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/testability-migration/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/testability-migration/AGENT.md @@ -6,7 +6,6 @@ description: >- Use when asked to make code testable, remove static coupling, migrate to TimeProvider, adopt IFileSystem, or improve testability of a legacy codebase. name: testability-migration -tools: ['read', 'search', 'edit', 'terminal', 'skill'] handoffs: - label: Generate Tests for Migrated Code agent: code-testing-generator diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-assertion-quality/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md similarity index 85% rename from catalog/Platform/Official-DotNet-Experimental/skills/exp-assertion-quality/SKILL.md rename to catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md index 551fa71..e3770ba 100644 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-assertion-quality/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md @@ -1,6 +1,6 @@ --- -name: exp-assertion-quality -description: "Analyzes the variety and depth of assertions across .NET test suites. Use when the user asks to evaluate assertion quality, find shallow testing, identify tests with only trivial assertions, measure assertion coverage diversity, or audit whether tests verify different facets of correctness. Produces metrics and actionable recommendations. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), detecting anti-patterns (use test-anti-patterns), or fixing existing assertions." +name: assertion-quality +description: "Analyzes the variety and depth of assertions across .NET test suites. Use when the user asks to evaluate assertion quality, find shallow testing, identify assertion-free tests (no assertions or only trivial ones like Assert.IsNotNull), flag self-referential or tautological assertions (output equals input on identity/round-trip operations), measure assertion coverage diversity, or audit whether tests verify different facets of correctness. Produces metrics and actionable recommendations. Works with MSTest, xUnit, NUnit, TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), other anti-patterns like flakiness or duplication (use test-anti-patterns), or fixing assertions." license: MIT --- @@ -47,7 +47,7 @@ Low assertion diversity signals shallow testing. Tests may pass while bugs hide ### Step 1: Gather the test code -Read all test files the user provides. If the user points to a directory or project, scan for all test files — see the `exp-dotnet-test-frameworks` skill for framework-specific markers. +Read all test files the user provides. If the user points to a directory or project, scan for all test files — see the `dotnet-test-frameworks` skill for framework-specific markers. ### Step 2: Classify every assertion @@ -83,6 +83,7 @@ Calculate these metrics for the test suite: - **Assertion type spread**: Number of distinct assertion categories used across the suite (out of 12) - **Tests with zero assertions**: Count and percentage of test methods with no assertions at all - **Tests with only trivial assertions**: Count and percentage of tests where every assertion is only a null check or `Assert.IsTrue(true)` — trivial means no meaningful value verification +- **Tests with self-referential assertions**: Count and percentage of tests whose assertions compare an input to a round-tripped or identity-transformed version of itself (e.g., `Assert.AreEqual(input, Parse(input.ToString()))`) or assert a field against itself (`Assert.AreEqual(dto.Name, dto.Name)`). These are tautological — they verify the plumbing, not the behavior. - **Tests with negative assertions**: Count and percentage (target: at least 10% of tests should verify what should NOT happen) - **Tests with exception assertions**: Count and percentage - **Tests with state/side-effect assertions**: Count and percentage @@ -98,6 +99,7 @@ Before reporting, calibrate findings: - **Consider the test's intent.** A test for a void method that verifies state change on a dependency is legitimate even if it only uses `Assert.IsTrue`. - **Exception tests are inherently low-assertion-count.** `Assert.ThrowsException(() => ...)` may be the only assertion — that's fine for exception-focused tests. Don't penalize them for low assertion count. - **Don't conflate diversity with volume.** A test with 20 `Assert.AreEqual` calls has high volume but low diversity. A test with one equality, one null check, and one exception assertion has low volume but good diversity. +- **Self-referential assertions are not meaningful equality checks.** `Assert.AreEqual(input, roundTrip(input))` looks like a real equality assertion but is tautological when the operation under test is expected to be identity. Flag these separately from normal equality assertions. If the test's *purpose* is to verify a round-trip (serialize/deserialize, encode/decode), the assertion is valid — but it should be accompanied by assertions on non-trivial inputs that exercise the transformation. - **If assertions are well-diversified, say so.** A report concluding the suite has good diversity is perfectly valid. ### Step 5: Report findings diff --git a/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/manifest.json b/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/manifest.json new file mode 100644 index 0000000..69b5926 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Testing", + "compatibility": "Requires a .NET test project or solution." +} diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-agent/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-agent/SKILL.md index 1250a47..b0e2e49 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/code-testing-agent/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-agent/SKILL.md @@ -1,15 +1,20 @@ --- name: code-testing-agent description: >- - Generates comprehensive, workable unit tests for any programming language - using a multi-agent pipeline. Use when asked to generate tests, write unit - tests, improve test coverage, add test coverage, or create test files. - Supports C#, TypeScript, JavaScript, Python, Go, Rust, Java, and more. - Orchestrates research, planning, and implementation phases to produce - tests that compile, pass, and follow project conventions. - DO NOT USE FOR: running existing tests, executing dotnet test, applying - test filters, detecting test platforms, or troubleshooting test execution - (use run-tests for all of these). + Generates and writes new unit tests for any programming language using a + Research-Plan-Implement pipeline. Use when asked to generate tests, + write unit tests, add tests, improve test coverage, create test + project, achieve high coverage, comprehensive tests, or asked to + scaffold a new test project for an app, service, or library. Supports + C#, TypeScript, JavaScript, Python, Go, Rust, Java, and more. Orchestrates + the code-testing-generator sub-agent through research, planning, and + implementation phases so tests compile, pass, and follow project + conventions. DO NOT USE FOR: running existing tests or test filters + (use run-tests); diagnosing coverage plateaus or project-wide + coverage/CRAP analysis without writing tests (use coverage-analysis); + targeted method/class CRAP scores (use crap-score); MSTest assertion + guidance, MSTest test pattern modernization, or fixing existing MSTest test + code (use writing-mstest-tests). license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/SKILL.md index cd74bfe..1874b3f 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/SKILL.md @@ -18,7 +18,16 @@ This skill provides access to language-specific guidance files used by the code- | File | Language | Contents | |------|----------|----------| | [extensions/dotnet.md](extensions/dotnet.md) | .NET (C#/F#/VB) | Build commands, test commands, project reference validation, common CS error codes, MSTest template | +| [extensions/python.md](extensions/python.md) | Python | Framework-adaptive test commands (pytest, custom runners), project layout detection, mocking guidelines, common errors | +| [extensions/typescript.md](extensions/typescript.md) | TypeScript/JavaScript | Build/test commands (Jest/Vitest/Mocha), framework detection, mocking, TS-specific considerations | +| [extensions/powershell.md](extensions/powershell.md) | PowerShell | Test commands (Pester v5), module import patterns, discovery/run pitfalls, mocking, common errors | | [extensions/cpp.md](extensions/cpp.md) | C++ | Testing internals with friend declarations | +| [extensions/go.md](extensions/go.md) | Go | `go test` commands, table-driven tests, integration vs unit layout, mocking via interfaces, common errors | +| [extensions/java.md](extensions/java.md) | Java | Maven/Gradle commands, JUnit 4/5 and TestNG detection, Mockito, Spring Boot slices, common errors | +| [extensions/rust.md](extensions/rust.md) | Rust | `cargo test` commands, unit vs integration vs doc tests, features, async test harnesses, common errors | +| [extensions/ruby.md](extensions/ruby.md) | Ruby | RSpec and Minitest commands, Bundler usage, Rails specifics, mocking patterns, common errors | +| [extensions/swift.md](extensions/swift.md) | Swift | SPM and Xcode test commands, XCTest vs Swift Testing, `@testable import`, async/throws tests, common errors | +| [extensions/kotlin.md](extensions/kotlin.md) | Kotlin | Gradle commands, JUnit/Kotest detection, MockK, coroutines test, KMP and Android specifics, common errors | | [extensions/dotnet-examples.md](extensions/dotnet-examples.md) | .NET (C#/F#/VB) | Concrete pipeline examples: sample research output, plan, generated tests, fix cycles, final report | ## Usage diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/dotnet.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/dotnet.md index 7c30a78..666fdb3 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/dotnet.md +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/dotnet.md @@ -71,6 +71,18 @@ If a new test project was created, register it with the solution so `dotnet test 4. Skip this if the project is already included in the solution or solution filter used for testing. 5. Prefer the researched test command. If you need to run the solution directly, use `dotnet test --solution ` only for repos on .NET SDK 10+ with MTP-style syntax; otherwise use the standard positional form `dotnet test `. +## Test Framework Detection + +Detect the framework from the test project's `.csproj` package references and match its conventions: + +| Package Reference | Framework | Attributes | Assertion Style | +|-------------------|-----------|------------|-----------------| +| `MSTest.Sdk` or `MSTest.TestFramework` | MSTest | `[TestClass]`, `[TestMethod]`, `[DataRow]` | `Assert.AreEqual(expected, actual)` | +| `xunit` | xUnit | `[Fact]`, `[Theory]`, `[InlineData]` | `Assert.Equal(expected, actual)` | +| `NUnit` | NUnit | `[TestFixture]`, `[Test]`, `[TestCase]` | `Assert.That(actual, Is.EqualTo(expected))` | + +Use the repo's existing framework — do not introduce a different one. + ## MSTest Template ```csharp @@ -110,4 +122,6 @@ public sealed class ClassNameTests ## Skip Coverage Tools -Do not configure or run code coverage measurement tools (coverlet, dotnet-coverage, XPlat Code Coverage). These tools have inconsistent cross-configuration behavior and waste significant time. Coverage is measured separately by the evaluation harness. +Do not configure or run code coverage measurement tools (coverlet, dotnet-coverage, XPlat Code Coverage) by default. These tools have inconsistent cross-configuration behavior and waste significant time. Coverage is measured separately by the evaluation harness. + +**Exception**: if the user or evaluation harness explicitly requires a Cobertura/XML coverage artifact (e.g., they ask for `coverlet.collector` or a `--collect:"XPlat Code Coverage"` run), add the `coverlet.collector` PackageReference to the generated .NET test csproj so the harness's coverage command can produce output. Do not run the coverage command yourself; leave that to the validation step. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/go.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/go.md new file mode 100644 index 0000000..f084767 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/go.md @@ -0,0 +1,158 @@ +# Go Extension + +Language-specific guidance for Go test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*_test.go` files and copy their style (table-driven layout, helper usage, assertion library, build tags) +2. **`go.mod` / `go.sum`** — module path, Go version, dependencies (e.g. `testify`, `gomock`, `mockery`) +3. **Build/CI scripts** — `Makefile`, `magefile.go`, `Taskfile.yml`, `.github/workflows/*.yml` +4. **`go.work`** — if present, you are in a workspace; tests for a module must run from that module's directory or use `-C` (Go 1.20+) + +Use whatever assertion style and test layout the repo already uses. Do not introduce `testify` if the repo uses the standard library only. + +## Toolchain Detection + +| Indicator | Meaning | +|-----------|---------| +| `go.mod` `go 1.x` directive | Minimum Go version — match it locally with `go version` | +| `go.work` at the root | Multi-module workspace; commands resolve dependent modules from sibling directories | +| `vendor/` directory | Vendored deps; many commands implicitly add `-mod=vendor` | +| `tools.go` with `//go:build tools` | Tool versions pinned in `go.mod` (e.g. `mockgen`); install with `go install` from the listed paths | + +## Build Commands + +| Scope | Command | +|-------|---------| +| Compile a package | `go build ./path/to/pkg` | +| Vet (static analysis) | `go vet ./...` | +| Compile tests without running | `go test -count=1 -run=^$ ./path/to/pkg` | +| Whole module | `go build ./...` | + +`go build ./...` is the closest thing to a "does it compile" gate. It does not exercise test files — use `go test -run=^$` to type-check tests as well. + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests in a package | `go test ./path/to/pkg` | +| All tests in module | `go test ./...` | +| Single test | `go test -run '^TestName$' ./path/to/pkg` | +| Subtest | `go test -run '^TestName$/^subname$' ./path/to/pkg` | +| Verbose | `go test -v ./path/to/pkg` | +| Race detector | `go test -race ./...` | +| Disable cache | `go test -count=1 ./...` | +| Short mode | `go test -short ./...` | + +- `-run` arguments are **regular expressions anchored** with `^...$`; without anchors the pattern matches as a substring +- `go test -count=1` is the canonical way to bypass the test result cache; never use a fake `-count=2` or environment hacks +- `-race` significantly slows tests and requires CGO — only enable if the repo's CI does + +## Lint Command + +Use the repo's lint script first (`make lint`, `task lint`). Otherwise detect from `.golangci.yml`/`.golangci.yaml`: + +- `.golangci.yml` present → `golangci-lint run ./...` +- No config → `gofmt -w .` and `go vet ./...` +- `goimports` config / pre-commit hook → `goimports -w path/to/file.go` + +Never disable existing linters in the test files you generate. + +## Project Layout and Imports + +Go uses package paths derived from the module path in `go.mod`. + +| Scenario | Test placement | Package declaration | +|----------|----------------|----------------------| +| Internal-only test (white-box) | `foo_test.go` next to `foo.go` | `package foo` (same as production) | +| External-only test (black-box) | `foo_test.go` next to `foo.go` | `package foo_test` (forces use of public API) | +| Integration / build-tag gated | `foo_integration_test.go` | Add `//go:build integration` at top | + +- Test files **must** end with `_test.go` — the toolchain ignores other names +- A package directory may contain both `package foo` and `package foo_test` test files simultaneously +- Helpers shared across tests in one package go in `helpers_test.go` — do not export them; put them in the `_test` package only if integration tests in another package need them +- Imports use the full module path: `import "github.com/org/module/pkg"` — copy the exact module path from `go.mod` + +## Test Function Signatures + +| Kind | Signature | +|------|-----------| +| Standard test | `func TestThing(t *testing.T)` | +| Subtests | `t.Run("name", func(t *testing.T) { ... })` | +| Benchmark | `func BenchmarkThing(b *testing.B)` | +| Example (godoc) | `func ExampleThing()` with `// Output:` comment | +| Fuzz (Go 1.18+) | `func FuzzThing(f *testing.F)` | +| Per-package setup | `func TestMain(m *testing.M)` — call `m.Run()` and `os.Exit` with its code | + +Use **table-driven tests** when generating multiple cases for the same behavior — this is idiomatic Go and matches what most repos already use: + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + want int + }{ + {"positives", 2, 3, 5}, + {"negatives", -1, -1, -2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Add(tt.a, tt.b); got != tt.want { + t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want) + } + }) + } +} +``` + +When iterating with `t.Run` over a loop variable on Go < 1.22, capture it with `tt := tt` to avoid closure-over-loop-variable bugs. + +## Common Errors + +| Error | Fix | +|-------|-----| +| `package X is not in std` / `cannot find module providing package X` | Add the import to `go.mod`: `go get path/to/module@version`, then `go mod tidy` | +| `import cycle not allowed in test` | Move shared helpers to a separate package, or switch to a `_test` package for black-box tests | +| `undefined: X` in `_test` package | The symbol is unexported; either use `package foo` (white-box) or export it intentionally | +| `t.Parallel called multiple times` | Each subtest can call `t.Parallel()` once; do not call it twice in the same test | +| `panic: test executed panic(nil) or runtime.Goexit` | A goroutine called `t.Fatal` outside the test goroutine; only the main test goroutine may call `Fatal`/`FailNow` | +| `flag provided but not defined: -X` | Flags registered in `init()` of test files must use `flag.NewFlagSet` carefully; place test-only flags in `TestMain` | +| `go: cannot find main module` | Run inside the module directory (where `go.mod` lives), or use `-C path` (Go 1.20+) | +| `build constraints exclude all Go files in...` | Build tags filtered out every file — match the repo's tag with `-tags=integration` etc. | +| `missing go.sum entry for module` | Run `go mod download` or `go mod tidy` | +| Race detector reports data race | Fix the race; do not silence it. CGO must be enabled | + +## Mocking Rules + +Go has no reflection-based mocking framework that's universally adopted. Pick what the repo already uses: + +- **Interfaces + hand-written fakes** (most idiomatic) — define a small interface in the consumer package and pass a struct that implements it +- **`gomock` / `mockgen`** — if the repo has `//go:generate mockgen ...` directives or `mocks/` directories, regenerate via `go generate ./...` rather than editing generated files +- **`testify/mock`** — used in many repos; instantiate with `new(MockX)` and chain `.On("Method", ...).Return(...)` +- **`httptest`** — for HTTP clients/servers; spin up `httptest.NewServer` instead of mocking `http.Client` + +Always prefer dependency injection over global function patching. If a test needs more than 3 mocks, flag it as a design smell. + +## Concurrency and Cleanup + +- Use `t.Cleanup(func() { ... })` instead of deferring in test bodies — runs even if `t.FailNow` fires +- Use `t.TempDir()` for temp files — auto-cleaned at test end +- Use `t.Context()` (Go 1.24+) or pass an explicit `context.Background()` — never call real network or filesystem APIs without one in long-running tests + +## Dependency Installation (Last Resort) + +Only install packages after investigation confirms they are missing: + +``` +go get github.com/stretchr/testify@latest +go mod tidy +``` + +Run `go mod tidy` after any `go get` to keep `go.sum` consistent. Never edit `go.sum` by hand. + +## Skip Coverage Tools + +Do not configure or run coverage tools (`-cover`, `-coverprofile`, `go tool cover`). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/java.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/java.md new file mode 100644 index 0000000..2a3e74d --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/java.md @@ -0,0 +1,198 @@ +# Java Extension + +Language-specific guidance for Java test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*Test.java` / `*Tests.java` / `*IT.java` (integration) files and copy their style (JUnit version, assertion library, mock library, lifecycle methods) +2. **Build file** — `pom.xml` (Maven), `build.gradle` / `build.gradle.kts` (Gradle), `BUILD` / `BUILD.bazel` (Bazel) +3. **Java version** — ``, `sourceCompatibility`, or `toolchains` block +4. **Wrapper scripts** — always prefer `./mvnw` or `./gradlew` over a system-installed Maven/Gradle so you match the project's pinned version + +Use whatever framework the repo already uses (JUnit 4, JUnit 5/Jupiter, TestNG). Do not migrate to a different framework as a side effect of writing tests. + +## Build Tool Detection + +| Indicator | Build tool | Default test command | +|-----------|------------|----------------------| +| `pom.xml` | Maven | `./mvnw test` | +| `build.gradle` / `build.gradle.kts` | Gradle | `./gradlew test` | +| `settings.gradle*` with `include 'subproject'` | Gradle multi-project | `./gradlew :subproject:test` | +| `BUILD` / `BUILD.bazel` | Bazel | `bazel test //path/to:test` | + +If both `pom.xml` and `build.gradle` exist, pick the one used by CI. + +## Build Commands + +| Scope | Maven | Gradle | +|-------|-------|--------| +| Compile main + test | `./mvnw test-compile` | `./gradlew testClasses` | +| Compile only | `./mvnw compile` | `./gradlew classes` | +| Full build | `./mvnw verify` | `./gradlew build` | +| Skip tests during build | `./mvnw -DskipTests package` | `./gradlew assemble` | + +- Use `-q` (Maven) / `--console=plain` (Gradle) to reduce output noise +- For Gradle, prefer `--no-daemon` only in CI; locally the daemon makes incremental builds far faster + +## Test Commands + +| Scope | Maven | Gradle | +|-------|-------|--------| +| All unit tests | `./mvnw test` | `./gradlew test` | +| Single class | `./mvnw test -Dtest=MyClassTest` | `./gradlew test --tests MyClassTest` | +| Single method | `./mvnw test -Dtest=MyClassTest#myMethod` | `./gradlew test --tests MyClassTest.myMethod` | +| Tag filter (JUnit 5) | `./mvnw test -Dgroups=fast` | `./gradlew test -PincludeTags=fast` (if configured) or `--tests` | +| Integration tests | `./mvnw verify -DskipUnitTests` (with failsafe-plugin) | `./gradlew integrationTest` (if registered) | + +- `Surefire` runs unit tests (`*Test.java`); `Failsafe` runs integration tests (`*IT.java`) — do not put long integration tests under Surefire +- Gradle's `--tests` accepts wildcards: `--tests "*MyMethod*"` +- Use `--rerun-tasks` (Gradle) or `-DforkCount=...` (Surefire) only when troubleshooting cache issues + +## Lint Command + +Use the repo's existing lint task first. Otherwise check for: + +- Checkstyle (`checkstyle.xml`, `checkstyle`) → `./mvnw checkstyle:check` or `./gradlew checkstyleMain` +- Spotless (`spotless` block / plugin) → `./mvnw spotless:apply` or `./gradlew spotlessApply` +- ErrorProne / NullAway → integrated into compilation; run a normal build +- google-java-format / palantir-java-format → use the repo's configured formatter + +Never disable existing checks in the test files you generate. + +## Project Layout and Imports + +Maven/Gradle conventional layout: + +``` +src/ +├── main/java/com/example/foo/Bar.java +├── main/resources/ +├── test/java/com/example/foo/BarTest.java +└── test/resources/ +``` + +| Layout | Test placement | +|--------|----------------| +| Standard | `src/test/java//Test.java` | +| Integration tests separated | `src/integrationTest/java/...` (Gradle) or `src/it/java/...` (Maven w/ failsafe) | +| Multi-module Maven | Tests live in the same module as the code under test | + +- Test classes must mirror the production class's **package** to access package-private members +- Avoid wildcard imports unless the repo already uses them — match the explicit imports shown in the templates below +- For JUnit 5: import `org.junit.jupiter.api.Test` (and other annotations as needed) and `org.junit.jupiter.api.Assertions.assertEquals` etc. as static imports +- For JUnit 4: import `org.junit.Test`, `org.junit.Before`, etc., and `org.junit.Assert.assertEquals` etc. as static imports + +## Test Framework Detection + +| Indicator | Framework | Annotations | Assertion style | +|-----------|-----------|-------------|------------------| +| `junit-jupiter-*` deps | JUnit 5 | `@Test`, `@ParameterizedTest`, `@BeforeEach`, `@DisplayName` | `Assertions.assertEquals(expected, actual)` | +| `junit:junit:4.x` | JUnit 4 | `@Test`, `@Before`, `@RunWith` | `Assert.assertEquals(expected, actual)` | +| `org.testng:testng` | TestNG | `@Test(groups=...)`, `@BeforeMethod` | `Assert.assertEquals(actual, expected)` (note **reversed** order) | +| `org.assertj:assertj-core` | AssertJ (assertions only) | n/a | `assertThat(actual).isEqualTo(expected)` | +| `org.hamcrest:hamcrest` | Hamcrest matchers | n/a | `assertThat(actual, is(equalTo(expected)))` | + +**Argument order matters**: JUnit/AssertJ use `(expected, actual)`; TestNG uses `(actual, expected)`. Reversing them produces confusing failure messages. + +## JUnit 5 Template + +```java +package com.example.foo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CalculatorTest { + + @Test + @DisplayName("add returns sum of two positive numbers") + void add_positiveNumbers_returnsSum() { + Calculator sut = new Calculator(); + assertEquals(5, sut.add(2, 3)); + } + + @ParameterizedTest + @CsvSource({ + "2, 3, 5", + "-1, 1, 0" + }) + void add_validInputs_returnsSum(int a, int b, int expected) { + assertEquals(expected, new Calculator().add(a, b)); + } + + @Test + void divide_byZero_throws() { + Calculator sut = new Calculator(); + assertThrows(ArithmeticException.class, () -> sut.divide(1, 0)); + } +} +``` + +## Common Errors + +| Error | Fix | +|-------|-----| +| `package X does not exist` | Add the dependency to `pom.xml` / `build.gradle`; run `./mvnw dependency:resolve` or `./gradlew --refresh-dependencies` | +| `cannot find symbol` | Verify class name and import path; check that the test source set sees the production source set | +| `No tests found for given includes` (Gradle) | `--tests` pattern doesn't match; verify the class/method names, that test methods are annotated with `@Test`, and that the class name matches the test task's `include` pattern (default `**/*Test*.class`). For JUnit 4 only, the class must also be `public` with a public no-arg constructor — JUnit 5 allows package-private classes and methods | +| `Test class should have exactly one public zero-argument constructor` (JUnit 4) | Remove constructors with parameters; use `@Before` for setup | +| `org.junit.runners.model.InvalidTestClassError` (JUnit 4) | Class is missing `public`, has wrong constructor, or method signature is wrong | +| Mixing `org.junit.Test` (4) and `org.junit.jupiter.api.Test` (5) | Pick one framework per test class — imports must match the framework annotation | +| `java.lang.NoClassDefFoundError` at runtime | Test runtime classpath is missing a transitive dep; add it to `testRuntimeOnly` (Gradle) or `test` (Maven) | +| `UnsupportedClassVersionError` | JDK used to run tests is older than the JDK used to compile; align toolchains | +| `Mockito cannot mock final class` | Use Mockito's inline mock maker — Mockito 5+ uses it by default; for Mockito 3.x/4.x add the `mockito-inline` artifact (replaces `mockito-core`). Or switch to MockK for Kotlin. `mockito-subclass` does **not** mock final classes | +| `WrongTypeOfReturnValue` (Mockito) | The stubbed method returns a different type than the mock was set up for — check return type signatures | + +## Mocking Rules + +- Use whatever the repo already uses: **Mockito** (most common), **EasyMock**, **JMockit**, or hand-written fakes +- For JUnit 5 + Mockito, use `@ExtendWith(MockitoExtension.class)` with `@Mock` / `@InjectMocks` fields +- For JUnit 4 + Mockito, use `@RunWith(MockitoJUnitRunner.class)` or `MockitoAnnotations.openMocks(this)` in `@Before` +- Use `when(mock.method(...)).thenReturn(...)` for stubs and `verify(mock).method(...)` for interactions +- Use `ArgumentCaptor` to assert on complex argument values rather than over-specifying matchers +- Prefer constructor injection so production code stays testable without `@InjectMocks` +- If a test needs more than 3 mocks, flag it as a design smell + +## Spring Boot + +If the repo uses Spring Boot: + +- `@SpringBootTest` loads the full context — slow; use only when needed +- Slice tests are faster: `@WebMvcTest`, `@DataJpaTest`, `@JsonTest` +- Use `@MockBean` (Spring) only inside Spring tests; in plain unit tests use `@Mock` +- Use `@Testcontainers` for real-DB integration tests if the repo already has it on the classpath + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing. + +Maven (`pom.xml`): + +```xml + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + +``` + +Gradle (`build.gradle.kts`): + +```kotlin +testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") +testRuntimeOnly("org.junit.platform:junit-platform-launcher") +``` + +If the repo uses BOMs (`` or Gradle platforms), reuse them — don't pin a different version than the BOM publishes. + +## Skip Coverage Tools + +Do not configure or run coverage tools (JaCoCo, Cobertura, OpenClover). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/kotlin.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/kotlin.md new file mode 100644 index 0000000..1bebd0a --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/kotlin.md @@ -0,0 +1,227 @@ +# Kotlin Extension + +Language-specific guidance for Kotlin test generation. For pure-Java codebases, use `java.md` instead. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find files in `src/test/kotlin/`, `src/commonTest/kotlin/`, `src/jvmTest/kotlin/`, etc., and copy their style (framework, assertion library, mock library, coroutine helpers) +2. **Build file** — `build.gradle.kts` / `build.gradle` — note Kotlin version, plugins (`kotlin("jvm")`, `kotlin("multiplatform")`, `kotlin("android")`), and `dependencies { testImplementation(...) }` +3. **`gradle/libs.versions.toml`** — the version catalog if the repo uses one; reference aliases instead of hard-coded versions +4. **Wrapper script** — always invoke `./gradlew` (Unix) or `.\gradlew.bat` (Windows), never a system-installed Gradle +5. **Multiplatform layout** — `src//kotlin/` indicates KMP; tests live in matching `*Test` source sets (`commonTest`, `jvmTest`, `nativeTest`) + +Use whatever framework the repo already uses (JUnit Jupiter, JUnit 4, Kotest, kotlin.test). Do not switch. + +## Project Type Detection + +| Indicator | Project type | +|-----------|--------------| +| `kotlin("jvm")` plugin | Plain JVM Kotlin | +| `kotlin("multiplatform")` plugin with `kotlin { jvm(); js(); ... }` | Kotlin Multiplatform | +| `com.android.application` / `com.android.library` plugin | Android | +| `org.springframework.boot` plugin | Spring Boot Kotlin | +| `kotlin("jvm")` + `application` plugin | Kotlin CLI / server | + +For **Android**, see also platform-specific test types: `src/test/` for unit tests on the JVM, `src/androidTest/` for instrumented tests on a device/emulator. They use different runners and gradle tasks. + +## Build Commands + +| Scope | Command | +|-------|---------| +| Compile main + test (JVM) | `./gradlew compileTestKotlin` | +| Full build | `./gradlew build` | +| Skip tests | `./gradlew assemble` | +| Single module | `./gradlew :module-name:build` | +| KMP target only | `./gradlew :module:jvmTest` (or `linuxX64Test`, etc.) | + +- Use `--console=plain` to suppress Gradle's animated output +- Use `--build-cache` (often default in CI) to reuse outputs +- For Android: `./gradlew assembleDebug` (build APK) and `./gradlew testDebugUnitTest` (run unit tests) + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests (JVM) | `./gradlew test` | +| Single class | `./gradlew test --tests "com.example.WidgetTest"` | +| Single method | `./gradlew test --tests "com.example.WidgetTest.add returns sum"` | +| KMP all targets | `./gradlew allTests` | +| KMP one target | `./gradlew jvmTest`, `./gradlew jsTest`, `./gradlew linuxX64Test` | +| Android unit tests | `./gradlew testDebugUnitTest` | +| Android instrumented | `./gradlew connectedDebugAndroidTest` (requires device/emulator) | + +- `--tests` accepts wildcards: `--tests "*Widget*"`. Method names with spaces or backticks must be quoted: `--tests "com.example.WidgetTest.creates a widget"` +- Use `--rerun-tasks` only when troubleshooting cache issues +- For Kotest, the runner is registered with JUnit Platform — the standard `./gradlew test` and `--tests` flags work the same way + +## Lint Command + +Use the repo's lint tooling first: + +- `./gradlew ktlintCheck` (autoformat: `./gradlew ktlintFormat`) when ktlint is configured +- `./gradlew detekt` when detekt is configured +- `./gradlew spotlessCheck` / `spotlessApply` for the Spotless plugin +- Android Studio's IDE inspections; `./gradlew lint` (Android-only) for the Android Lint task + +## Project Layout + +``` +src/ +├── main/kotlin/com/example/foo/Bar.kt +├── main/resources/ +├── test/kotlin/com/example/foo/BarTest.kt # mirrors production package +└── test/resources/ +``` + +KMP layout: + +``` +src/ +├── commonMain/kotlin/... # shared +├── commonTest/kotlin/... # shared tests using kotlin.test +├── jvmMain/kotlin/... +├── jvmTest/kotlin/... +├── jsMain/kotlin/... +└── jsTest/kotlin/... +``` + +- Test classes mirror the production class's package so they can access `internal` members (Kotlin's `internal` is module-scoped — within the same Gradle module, including the test source set) +- For KMP common tests, you can only import from `kotlin.test` and other multiplatform-aware libraries (e.g. `kotlinx.coroutines.test`, Kotest multiplatform, MockK on JVM only) + +## Test Framework Detection + +| Dependency | Framework | Annotations / DSL | +|------------|-----------|--------------------| +| `org.jetbrains.kotlin:kotlin-test` | kotlin.test (multiplatform) | `@Test`, `@BeforeTest`, `assertEquals`, `assertFailsWith` | +| `junit-jupiter-*` | JUnit 5 | `@Test`, `@ParameterizedTest`, `@BeforeEach`, `@DisplayName` | +| `junit:junit:4.x` | JUnit 4 | `@Test`, `@Before`, `@RunWith(JUnitPlatform::class)` rare | +| `io.kotest:kotest-runner-junit5` | Kotest | `class FooSpec : FunSpec({ test("...") { ... } })` (DSL — many styles: `StringSpec`, `BehaviorSpec`, etc.) | +| `org.spekframework.spek2:spek-dsl-jvm` | Spek 2 | `object FooSpec : Spek({ describe(...) { it(...) {} } })` (legacy) | + +For Kotest, **stick to the spec style the repo already uses** — mixing styles is confusing. + +## Test Templates + +### JUnit 5 + +```kotlin +package com.example.foo + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals + +class CalculatorTest { + + @Test + @DisplayName("add returns sum of two positive numbers") + fun `add returns sum of two positives`() { + val sut = Calculator() + assertEquals(5, sut.add(2, 3)) + } + + @Test + fun `divide by zero throws`() { + val sut = Calculator() + assertThrows { sut.divide(1, 0) } + } +} +``` + +Backticked method names (`` `like this` ``) are idiomatic for Kotlin tests because they read better in failure messages. + +### Kotest (StringSpec) + +```kotlin +package com.example.foo + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow + +class CalculatorSpec : StringSpec({ + "add returns sum of two positive numbers" { + Calculator().add(2, 3) shouldBe 5 + } + + "divide by zero throws" { + shouldThrow { Calculator().divide(1, 0) } + } +}) +``` + +## Coroutines + +- Use `kotlinx-coroutines-test` when it's already on the classpath; otherwise add it as a `testImplementation` only after confirming it is missing (see Dependency Installation) +- Use `runTest { ... }` (replaces the older `runBlockingTest`) for `suspend` test bodies +- For virtual time advance, use a `TestDispatcher` built from `testScheduler` — e.g. `StandardTestDispatcher(testScheduler)` or `UnconfinedTestDispatcher(testScheduler)` — rather than calling `delay` and waiting in real time +- Inject a `CoroutineDispatcher` into production code instead of using `Dispatchers.Main/IO` directly — then swap it in tests via `Dispatchers.setMain(testDispatcher)` + +```kotlin +@Test +fun `loads data eventually`() = runTest { + val repo = FakeRepo() + val dispatcher = StandardTestDispatcher(testScheduler) + val sut = Loader(repo, dispatcher) + sut.start() + advanceUntilIdle() + assertEquals(LoadState.Done, sut.state.value) +} +``` + +## Common Errors + +| Error | Fix | +|-------|-----| +| `Unresolved reference: X` | Add the import; verify the test source set sees the production source set; for KMP, the dep may be declared only in `jvmTest` | +| `Cannot access 'X': it is internal in module Y` | `internal` is module-scoped, so a test in another Gradle module cannot see it. Move the test into the same module, expose a public seam (e.g. a `*-testing` artifact, or change visibility deliberately), or add the consuming module to the source module's `friend modules` via the Kotlin compiler `-Xfriend-paths` option. `@VisibleForTesting` does **not** widen Kotlin visibility | +| `Class 'XTest' is not abstract and does not implement abstract member` (Kotest spec) | The spec class needs a no-arg constructor and a primary-constructor block — match the existing spec style | +| `No tests found for given includes` (Gradle) | `--tests` pattern doesn't match; verify class name and that the framework's runner is registered on the test task (`useJUnitPlatform()`) | +| `kotlin.UninitializedPropertyAccessException: lateinit property X has not been initialized` | The `@BeforeEach` (or `BeforeTest`) didn't run, or the field was reset; use `lateinit` only after confirming the lifecycle hook fires | +| `IllegalStateException: Module with the Main dispatcher had failed to initialize` | Coroutines test needs `Dispatchers.setMain(...)` before launching anything that touches `Dispatchers.Main`; reset with `Dispatchers.resetMain()` in teardown | +| `Mockito cannot mock final class` | Kotlin classes are `final` by default — either use **MockK** (works with final classes) or apply the `kotlin-allopen` plugin scoped to a marker annotation | +| `MissingMockKException` | The mock wasn't initialized; call `MockKAnnotations.init(this)` or use `@MockK` with `@MockKExtension` (JUnit 5) | +| KMP common test references a JVM-only API | Move the test to `jvmTest`, or use `expect/actual` declarations | +| Android: `Method ... not mocked` | The unit test runs on the JVM and the SDK class is just a stub — either use Robolectric, move the test to instrumented (`androidTest`), or refactor to inject the dependency | + +## Mocking Rules + +- **MockK** is the de-facto standard for Kotlin (final classes, coroutine support): `every { mock.foo() } returns 1`, `coEvery { mock.suspendFn() } returns 1`, `verify { mock.foo() }`, `coVerify { ... }` +- Mockito works on Kotlin too with `mockito-kotlin` extensions, but Kotlin classes are `final` by default — use Mockito's inline mock maker (default in Mockito 5+; the `mockito-inline` artifact for Mockito 3.x/4.x). `mockito-subclass` cannot mock final classes +- Avoid `mockkStatic`/`mockkObject` for production code you control — refactor to a wrapper instead +- Prefer constructor injection so you don't need framework annotations (`@InjectMocks`) at all +- If a test needs more than 3 mocks, flag it as a design smell + +## Android Specifics + +- Robolectric tests live under `src/test/` and emulate the Android framework on the JVM — fast but imperfect +- Instrumented tests live under `src/androidTest/`, require a connected device/emulator, and are slow — use sparingly +- Compose UI tests use `createComposeRule()` and `composeTestRule.onNodeWithText(...).performClick()` — match the existing test setup if Compose is in the project +- Hilt: use `@HiltAndroidTest` and `HiltAndroidRule` for instrumented tests; for unit tests pass fakes directly to ViewModels + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing. + +`build.gradle.kts`: + +```kotlin +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testImplementation("io.mockk:mockk:1.13.10") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") +} + +tasks.test { + useJUnitPlatform() +} +``` + +If the repo uses a version catalog, add to `gradle/libs.versions.toml` and reference via `libs.junit.jupiter` etc. Match the major versions already in use. + +## Skip Coverage Tools + +Do not configure or run coverage tools (JaCoCo, Kover). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/powershell.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/powershell.md new file mode 100644 index 0000000..e0b1201 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/powershell.md @@ -0,0 +1,110 @@ +# PowerShell Extension + +Language-specific guidance for PowerShell test generation using Pester v5. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*.Tests.ps1` files and copy their style (structure, assertions, mock approach, import method) +2. **Module structure** — look for `.psd1` (manifest), `.psm1` (root module), `Public/`/`Private/` organization +3. **Build/test scripts** — check for `build.ps1`, `Invoke-Build` (`*.build.ps1`), `psake`, or CI scripts +4. **Shell target** — check `.psd1` for `PowerShellVersion`/`CompatiblePSEditions`, CI matrix for `pwsh` vs `powershell.exe` + +Use the repo's existing test conventions. Only add Pester if the repo has no tests at all. + +## Build Commands + +PowerShell is interpreted — no build step. If the repo has a build script, use it. Otherwise validate with: + +- **Module loads**: `Import-Module ./MyModule.psd1 -Force -ErrorAction Stop` +- **Script analyzer**: `Invoke-ScriptAnalyzer -Path ./src -Recurse` (if PSScriptAnalyzer is available) +- **Lint**: `Invoke-ScriptAnalyzer -Path path/to/file.ps1 -Fix` + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests | `Invoke-Pester` | +| Specific file | `Invoke-Pester -Path ./Tests/Get-Widget.Tests.ps1` | +| Filter by name | `Invoke-Pester -FullNameFilter '*Get-Widget*'` | +| Filter by tag | `Invoke-Pester -TagFilter 'Unit'` | +| Non-interactive (CI) | `Invoke-Pester -CI` | +| Detailed output | `Invoke-Pester -Output Detailed` | + +- Prefer the repo's build/test script over raw `Invoke-Pester` +- Use `-Output Detailed` during fix cycles, `-Output Minimal` for final validation + +## Project Layout and Imports + +| Layout | Import in `BeforeAll` | +|--------|-----------------------| +| Module (`.psd1`) | `Import-Module "$PSScriptRoot/../MyModule.psd1" -Force` | +| Library script (defines functions) | `. $PSScriptRoot/Get-Widget.ps1` | +| Co-located test | `. $PSCommandPath.Replace('.Tests.ps1', '.ps1')` | +| Executable script (has `param()`) | Do **not** dot-source — invoke with `& $PSScriptRoot/script.ps1 -Param value` and assert on output/errors | + +- **All imports go in `BeforeAll`** — never at script top level +- **Use `$PSScriptRoot` or `$PSCommandPath`** — never `$MyInvocation.MyCommand.Path` (returns empty in `BeforeAll`) +- Use `-Force` on `Import-Module` to pick up changes between runs + +## Test File Naming + +- Files: `*.Tests.ps1` — match existing convention (co-located vs `Tests/` directory) + +## Pester v5 Discovery vs Run (Critical) + +Pester v5 runs in **two phases**: Discovery (collects test metadata) then Run (executes tests). This is the #1 source of agent errors. + +**Rules:** +- All setup code goes in `BeforeAll` or `BeforeEach` — never at script top level or loose inside `Describe`/`Context` +- Code directly inside `Describe`/`Context` (but outside `It`/`Before*`/`After*`) runs during **Discovery** — do not put setup, imports, or variable assignments there +- Data for `-ForEach` / `-TestCases` must be set in `BeforeDiscovery`, not `BeforeAll` (BeforeAll runs after discovery) +- `-Skip:$condition` evaluates at Discovery time — conditions from `BeforeAll` will be `$null` +- Use `foreach` loops for dynamic test generation only with `BeforeDiscovery` data +- Use `TestDrive:` for file-based tests instead of touching repo files — Pester cleans it up automatically + +## Common Errors + +| Error | Fix | +|-------|-----| +| Variable is `$null` in `It` block | Move assignment into `BeforeAll` — variables set there are visible to child `It` blocks without `$script:` | +| `-ForEach` data is empty | Move data setup from `BeforeAll` to `BeforeDiscovery` | +| `CommandNotFoundException` for Mock target | The function must exist before mocking — import the module in `BeforeAll` first | +| `$MyInvocation.MyCommand.Path` returns empty | Use `$PSCommandPath` or `$PSScriptRoot` instead | +| `Should Be` (no dash) fails | Use v5 syntax: `Should -Be` (with dash prefix) | +| `Assert-MockCalled` not recognized | Use v5 syntax: `Should -Invoke` | +| Mock has no effect | Check scope — mocks in `It` only apply to that `It`; use `BeforeAll`/`BeforeEach` for broader scope | +| `Should -Throw` doesn't catch cmdlet errors | Most cmdlet errors are non-terminating — wrap with `{ cmd -ErrorAction Stop }` or set `$ErrorActionPreference = 'Stop'` in `BeforeEach` | +| Tests pass on Windows but fail on Linux | Use `Join-Path` not string concatenation; match exact file casing; avoid Windows-only cmdlets (Registry, EventLog) | + +## Mocking Rules + +- Place mocks in `BeforeAll` (shared) or `BeforeEach` (reset per test) +- Mock where the command is **called from** — use `-ModuleName` to mock inside a module's scope +- Use `-ParameterFilter` for selective mocking (no `param()` block needed in v5) +- Verify calls with `Should -Invoke` — default scope inside `It` counts only that test's calls +- Use `InModuleScope` sparingly and as narrowly as possible — prefer `Mock -ModuleName` for testing via public API +- Inside mock bodies, use `$PesterBoundParameters` not `$PSBoundParameters` +- If a test needs more than 3 mocks, flag it as a design smell + +## Non-Obvious Assertions + +Most `Should` operators are self-explanatory. These are the ones agents get wrong: + +- `Should -Throw` requires a **scriptblock**: `{ risky-op } | Should -Throw` — not a direct call +- `Should -Contain` is for **collections** — use `Should -Be` for scalar equality +- `Should -HaveParameter` validates cmdlet signatures: `Get-Command X | Should -HaveParameter 'Name' -Mandatory` +- `Should -Invoke` verifies mock calls: `Should -Invoke Get-Item -Times 1 -Exactly` + +## Cross-Platform + +- Prefer `pwsh` (PowerShell 7+) unless the repo explicitly targets Windows PowerShell 5.1 +- Use `Join-Path` for paths — never string concatenation with `\` +- Linux/macOS file systems are **case-sensitive** — match exact casing in imports and paths +- Windows ships Pester 3.4.0 — if v5 is needed: `Install-Module Pester -Force -SkipPublisherCheck` +- Check `$PSVersionTable.PSEdition` to detect Core vs Desktop + +## Skip Coverage Tools + +Do not configure or run coverage tools (Pester CodeCoverage, JaCoCo export). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/python.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/python.md new file mode 100644 index 0000000..6584e52 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/python.md @@ -0,0 +1,132 @@ +# Python Extension + +Language-specific guidance for Python test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, discover what the repo already does: + +1. **Find ALL existing test files** — search broadly: `test_*.py`, `*_test.py`, `*.uts`, `test/*.sh`, or any other test format. Do not assume pytest. +2. **Identify the test framework** — look for: + - Custom test runners (e.g. `UTscapy` for scapy, project-specific harnesses) + - Standard frameworks (`pytest`, `unittest`, `nose2`) + - Test runner scripts in `Makefile`, `tox.ini`, `nox`, `scripts/` + - Config entries in `pyproject.toml`, `setup.cfg`, `pytest.ini`, `conftest.py` +3. **Read existing tests thoroughly** — copy their exact style: file format, imports, fixtures, assertion patterns, helper utilities, setup/teardown conventions +4. **Package layout** — determine import paths from existing code, not guesswork + +**Use whatever framework and conventions the repo already uses.** If the repo uses a custom test framework (custom file formats, custom runners, domain-specific test utilities), adopt it fully — do not layer pytest on top. Only introduce pytest if the repo has no tests at all. + +## Environment Detection + +Detect the runner from lockfiles/config and prefix all commands accordingly: + +| Indicator | Prefix | +|-----------|--------| +| `poetry.lock` / `[tool.poetry]` in `pyproject.toml` | `poetry run` | +| `pdm.lock` / `[tool.pdm]` in `pyproject.toml` | `pdm run` | +| `uv.lock` / `[tool.uv]` in `pyproject.toml` | `uv run` | +| `Pipfile.lock` | `pipenv run` | +| `hatch.toml` / `[tool.hatch]` in `pyproject.toml` | `hatch run` | +| None of the above | `python -m` | + +If `Makefile`, `tox.ini`, or `nox` config exists, prefer those scripts over raw commands. + +## Build Commands + +Python has no separate build step. Validate with the type checker if one is configured: + +| Scope | Command | +|-------|---------| +| Syntax check | ` py_compile path/to/file.py` | +| Type check | ` mypy path/to/file.py` or ` pyright path/to/file.py` | + +## Test Commands + +If the repo uses a **custom test framework** (custom file formats, custom runner), use its native commands — do not wrap them in pytest. Examples: + +| Framework | Command | +|-----------|---------| +| UTscapy (`.uts` files) | ` scapy.tools.UTscapy -f test/test_file.uts` | +| Custom runner script | `make test`, `./run_tests.sh`, `tox` | +| Repo-defined script | Whatever `scripts.test` in Makefile/tox/nox specifies | + +For **pytest** projects (the most common case), use the detected ``: + +| Scope | Command | +|-------|---------| +| All tests | ` pytest` | +| Specific file | ` pytest tests/test_module.py` | +| Specific test | ` pytest tests/test_module.py::TestClass::test_method` | +| Keyword filter | ` pytest -k "keyword"` | +| Stop on first failure | ` pytest -x --tb=short` | + +- Prefer `python -m pytest` over bare `pytest` to ensure the correct interpreter +- If the project uses `unittest` only (no pytest in deps), use `python -m unittest discover` + +## Lint Command + +Use the repo's existing lint script first (`make lint`, `tox -e lint`). Otherwise detect tools from config: + +- `ruff.toml` or `[tool.ruff]` → ` ruff check --fix && ruff format` +- `[tool.black]` → ` black` +- `.flake8` → ` flake8` + +## Project Layout and Imports + +| Layout | Import Style | +|--------|-------------| +| `src/package/module.py` | `from package.module import X` | +| `package/module.py` at root | `from package.module import X` | +| `module.py` at root | `from module import X` | + +- **Match existing test imports exactly** — do not invent `src.` prefixes unless existing tests use them +- Check `pyproject.toml` `[tool.setuptools.package-dir]` for layout hints +- Default test placement: `tests/` mirroring source structure (`src/billing/service.py` → `tests/billing/test_service.py`) + +## Test File Naming + +Match the repo's existing conventions. Common patterns: + +- **pytest**: Files `test_*.py` or `*_test.py`, functions `test_` prefix, classes `Test` prefix +- **Custom frameworks**: Use whatever format existing tests use (e.g. `.uts` for UTscapy, custom extensions) + +If writing new tests in a repo with no tests, default to pytest conventions. + +## Common Errors + +| Error | Fix | +|-------|-----| +| `ModuleNotFoundError: No module named 'src'` | Import from the package name used by the repo, not from `src` | +| `ModuleNotFoundError: No module named 'X'` | Check existing imports for the correct package name; if editable install needed: ` pip install -e .` | +| `ImportError: attempted relative import` | Convert to absolute imports matching existing test patterns | +| `fixture 'X' not found` | Check `conftest.py` for existing fixtures; reuse them instead of creating new ones | +| `TypeError: missing required argument` | Read the full `__init__`/function signature; pass all required parameters | +| `async def functions are not natively supported` | Use `@pytest.mark.asyncio` only if `pytest-asyncio` is already in deps; check for `asyncio_mode = "auto"` in config | +| `SyntaxError` | Fix syntax at the indicated line | + +## Mocking Rules + +- Use `unittest.mock` (stdlib) — no extra dependency needed +- **Patch where the name is looked up**, not where it is defined: `@patch("mypackage.module.datetime")` not `@patch("datetime.datetime")` +- Use `Mock(spec=RealClass)` to catch attribute errors +- Use `AsyncMock` for async functions +- Prefer dependency injection over `@patch` +- If a test needs more than 3 mocks, flag it as a design smell + +## Dependency Installation (Last Resort) + +Only install packages after investigation confirms they are missing. Use the detected prefix: + +| Manager | Install command | +|---------|----------------| +| Poetry | `poetry add --group dev pytest` | +| PDM | `pdm add -dG test pytest` | +| uv | `uv add --dev pytest` | +| pip | `python -m pip install -e ".[dev]"` | + +Never run bare `pip install` in a Poetry/PDM/uv project — it bypasses the lockfile. + +## Skip Coverage Tools + +Do not configure or run coverage tools (coverage.py, pytest-cov). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/ruby.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/ruby.md new file mode 100644 index 0000000..6307f68 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/ruby.md @@ -0,0 +1,191 @@ +# Ruby Extension + +Language-specific guidance for Ruby test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `spec/**/*_spec.rb` (RSpec) or `test/**/*_test.rb` (Minitest) and copy their style (matchers, helpers, factories, contexts) +2. **`Gemfile` / `Gemfile.lock`** — Ruby version, test framework, supporting gems (`rspec`, `minitest`, `factory_bot`, `webmock`, `vcr`, `rails`) +3. **`.ruby-version`** / `.tool-versions` — pinned Ruby version +4. **Test helpers** — `spec/spec_helper.rb`, `spec/rails_helper.rb`, `test/test_helper.rb` — these dictate the load path, requires, and global config +5. **Rake tasks** — `Rakefile` may define a `default` task that runs the full test suite + +Use the framework the repo already uses. Do not introduce RSpec into a Minitest project (or vice versa). + +## Toolchain Detection + +| Indicator | Manager | Run prefix | +|-----------|---------|------------| +| `Gemfile.lock` | Bundler | `bundle exec ` | +| `.ruby-version` + `rbenv` | rbenv | combine with `bundle exec` | +| `mise.toml` / `asdf` `.tool-versions` | mise/asdf | the wrapper handles version selection; still use `bundle exec` | +| Plain Ruby, no Bundler | system Ruby | `ruby ` (rare in real projects) | + +Always run inside `bundle exec` if a `Gemfile.lock` is present — otherwise you may pick up a system gem version that disagrees with the lockfile. + +## Build Commands + +Ruby is interpreted — there is no compile step. The closest validations: + +| Scope | Command | +|-------|---------| +| Syntax check | `ruby -c path/to/file.rb` | +| Lint (RuboCop) | `bundle exec rubocop path/to/file.rb` | +| Type check (Sorbet) | `bundle exec srb tc` (only if `sorbet/` dir exists) | +| Type check (RBS/Steep) | `bundle exec steep check` | + +For Rails: load all classes once with `bundle exec rails zeitwerk:check` to catch missing constants before running tests. + +## Test Commands + +### RSpec + +| Scope | Command | +|-------|---------| +| All specs | `bundle exec rspec` | +| Single file | `bundle exec rspec spec/models/widget_spec.rb` | +| Single line | `bundle exec rspec spec/models/widget_spec.rb:42` | +| By name | `bundle exec rspec -e "creates a widget"` | +| Tagged | `bundle exec rspec --tag focus` | +| Fail fast | `bundle exec rspec --fail-fast` | +| Documentation format | `bundle exec rspec --format documentation` | + +### Minitest + +| Scope | Command | +|-------|---------| +| All tests | `bundle exec rake test` (Rails) or `bundle exec ruby -Ilib -Itest -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }'` | +| Single file | `bundle exec ruby -Itest test/models/widget_test.rb` | +| Single test | `bundle exec ruby -Itest test/models/widget_test.rb -n test_creates_widget` | +| By name pattern | `... -n /pattern/` | + +### Rails (any framework) + +| Scope | Command | +|-------|---------| +| Default suite | `bin/rails test` (Minitest) or `bundle exec rspec` | +| Single Rails test file | `bin/rails test test/models/widget_test.rb:42` | +| System tests | `bin/rails test:system` | + +Always prefer the wrapper script (`bin/rails`, `bin/rspec`) when present — they enforce the project's loader/setup. + +## Lint Command + +- `bundle exec rubocop` — autocorrect with `bundle exec rubocop -A` (only if existing tests already conform; do not autocorrect unrelated files) +- `bundle exec standardrb --fix` if `standard` is in the Gemfile +- Some Rails projects add `rubocop-rails`, `rubocop-rspec`, `rubocop-performance` — they enforce extra rules + +## Project Layout and Loading + +| Layout | Test placement | +|--------|----------------| +| Plain gem (RSpec) | `spec/` mirrors `lib/` (e.g. `lib/foo/bar.rb` → `spec/foo/bar_spec.rb`) | +| Plain gem (Minitest) | `test/` mirrors `lib/` (e.g. `test/foo/bar_test.rb`) | +| Rails (RSpec) | `spec/models`, `spec/controllers`, `spec/requests`, `spec/system`, etc. | +| Rails (Minitest) | `test/models`, `test/controllers`, `test/integration`, `test/system` | + +**Loading source code:** + +- RSpec: `spec/spec_helper.rb` typically does `require 'my_gem'` or sets `$LOAD_PATH`. Match its pattern in new specs by `require 'spec_helper'` (or `require 'rails_helper'` in Rails) +- Minitest: each `_test.rb` typically `require 'test_helper'` +- Rails uses Zeitwerk autoloading — do **not** add `require_relative '../../app/models/widget'`; just `require 'rails_helper'` and reference the constant + +## Test File Naming + +| Framework | File suffix | Class/example | +|-----------|-------------|---------------| +| RSpec | `_spec.rb` | `RSpec.describe Widget do ... end`, `it "..." do ... end` | +| Minitest (classic) | `_test.rb` | `class WidgetTest < Minitest::Test`, methods `def test_...` | +| Minitest (spec) | `_test.rb` | `describe Widget do ... it "..." do ... end end` | +| Rails Minitest | `_test.rb` | `class WidgetTest < ActiveSupport::TestCase` | + +## RSpec Template + +```ruby +require 'spec_helper' +require 'calculator' + +RSpec.describe Calculator do + subject(:calculator) { described_class.new } + + describe '#add' do + it 'returns the sum of two positive numbers' do + expect(calculator.add(2, 3)).to eq(5) + end + + context 'with negative numbers' do + it 'returns the correct sum' do + expect(calculator.add(-1, 1)).to eq(0) + end + end + + it 'raises when given non-numeric input' do + expect { calculator.add('a', 1) }.to raise_error(TypeError) + end + end +end +``` + +## Common Errors + +| Error | Fix | +|-------|-----| +| `LoadError: cannot load such file -- foo` | Missing `require` or load path; check `spec_helper.rb` for the established pattern instead of patching `$LOAD_PATH` ad hoc | +| `NameError: uninitialized constant X` | Constant isn't loaded — in Rails, ensure you require `rails_helper`; in plain Ruby, add the appropriate `require` | +| `ArgumentError: wrong number of arguments (given X, expected Y)` | Read the method signature; pass keyword vs positional args correctly | +| `NoMethodError: undefined method 'foo' for nil:NilClass` | Test setup left a value `nil`; check `let`/`before` ordering and factory data | +| `Failure/Error: ... received :foo with unexpected arguments` (RSpec) | Tighten the matcher: `with(hash_including(...))` or relax to `with(any_args)` deliberately | +| `expected #<...> to receive :foo (1 time) but received it 0 times` | Either the code path didn't call the stub, or you stubbed the wrong receiver | +| `DEPRECATION WARNING` (Rails) | Address the deprecation rather than silencing it; tests that warn today break tomorrow | +| `ActiveRecord::PendingMigrationError` | Run `bin/rails db:migrate RAILS_ENV=test` before tests | +| `Mysql2::Error / PG::ConnectionBad` in CI | Tests need a database — check `config/database.yml` and CI service containers | +| `Capybara::ElementNotFound` (system tests) | Use `find` with explicit waits; do not add `sleep` | + +## Mocking Rules (RSpec) + +- Use `instance_double(Klass)` and `class_double(Klass)` — they verify that the method actually exists, unlike `double` +- `allow(obj).to receive(:method).and_return(value)` for stubs; `expect(obj).to receive(:method)` for interaction expectations +- Prefer `instance_double` over plain `double`; prefer dependency injection over `allow_any_instance_of` +- Use `let` for memoized helpers; use `let!` only when the side effect must run before each example +- Avoid global state mutation in tests — wrap in `around` blocks or use `ClimateControl` for env vars +- For HTTP, use `webmock` (`stub_request(:get, ...)`) or `vcr` cassettes if the project already uses them +- If a test needs more than 3 mocks, flag it as a design smell + +## Mocking Rules (Minitest) + +- Use `Minitest::Mock` for simple cases: `mock = Minitest::Mock.new; mock.expect(:method, return_value, [arg])` +- For richer mocking, projects commonly add `mocha`: `obj.expects(:method).returns(value)` (in `test_helper.rb`: `require 'mocha/minitest'`) +- Always verify mocks at end of test (`mock.verify` for `Minitest::Mock`); Mocha verifies automatically + +## Rails Specifics + +- Use the **smallest** spec type that covers the behavior: model spec for pure logic, request spec for HTTP, system spec only when JS/UI matters +- `rails-controller-testing` gem must be present for `assigns(:foo)` and `assert_template` +- `ActiveJob::TestHelper` and `ActiveSupport::Testing::TimeHelpers` (`travel_to`) come with Rails — use them instead of `Timecop` if Rails ≥ 5 +- Use fixtures only if the project already uses them; `factory_bot` is more common in modern Rails apps +- Database transactions wrap each test by default — for system tests with browser drivers, use `DatabaseCleaner` strategies the project already configures + +## Dependency Installation (Last Resort) + +Only add gems after investigation confirms they are missing. Edit `Gemfile`: + +```ruby +group :test do + gem 'rspec' + gem 'webmock' +end +``` + +Then run: + +``` +bundle install +``` + +Never `gem install` outside Bundler — it bypasses the lockfile and changes the global Ruby environment. + +## Skip Coverage Tools + +Do not configure or run coverage tools (SimpleCov). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/rust.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/rust.md new file mode 100644 index 0000000..b383435 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/rust.md @@ -0,0 +1,180 @@ +# Rust Extension + +Language-specific guidance for Rust test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — look at `#[cfg(test)] mod tests` blocks inside `src/`, integration tests in `tests/`, doc tests in source comments, and any `examples/` that double as smoke tests +2. **`Cargo.toml`** — workspace layout (`[workspace]`), edition, `dev-dependencies`, feature flags, `[[bench]]` / `[[test]]` declarations +3. **`Cargo.lock`** — if checked in, you must not break it without intent +4. **Toolchain** — `rust-toolchain.toml` pins the channel (stable / nightly / specific version) +5. **`build.rs`** — custom build scripts may set `cfg` flags or generate code that tests rely on + +Match the repo's existing conventions — assertion macros, mock approach, feature-gating — exactly. Do not introduce `tokio::test` if the repo uses `async-std`, etc. + +## Toolchain Detection + +| Indicator | Meaning | +|-----------|---------| +| `rust-toolchain.toml` with `channel = "..."` | Use rustup to install/select that channel — `rustup show active-toolchain` | +| `rust-version = "1.x"` in `Cargo.toml` | Minimum supported Rust version (MSRV); do not use newer language features | +| `[workspace]` in root `Cargo.toml` | Multi-crate workspace; commands accept `-p ` to target one member | +| `nightly` channel | Tests may use `#![feature(...)]` flags; do not remove them | + +## Build Commands + +| Scope | Command | +|-------|---------| +| Type-check fast | `cargo check` | +| Type-check whole workspace | `cargo check --workspace --all-targets` | +| Build (debug) | `cargo build` | +| Build with all features | `cargo build --all-features` | +| Build a single crate | `cargo build -p crate-name` | +| Build tests without running | `cargo test --no-run` | + +`cargo check` is far faster than `cargo build` and catches almost the same errors. Prefer it during the fix loop; use `cargo build --tests` (or `cargo test --no-run`) before declaring tests compilable. + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests | `cargo test` | +| Workspace | `cargo test --workspace` | +| Single crate | `cargo test -p crate-name` | +| Filter by name | `cargo test substring_of_test_name` | +| Exact name | `cargo test -- --exact path::to::test_fn` | +| Single integration file | `cargo test --test file_stem` (no `.rs`) | +| Doc tests only | `cargo test --doc` | +| Show stdout | `cargo test -- --nocapture` | +| Single-threaded | `cargo test -- --test-threads=1` | +| Ignored tests | `cargo test -- --ignored` | +| With features | `cargo test --features "feat1 feat2"` | +| All features | `cargo test --all-features` | + +- Arguments before `--` are for cargo; arguments after `--` go to the test binary +- `cargo test foo` runs every test with `foo` in its full path (`module::tests::foo_does_a_thing`) — to avoid surprise matches use `--exact` +- `cargo nextest run` is significantly faster if the repo already uses it (`Cargo.toml` `[profile.nextest...]` or `.config/nextest.toml`) — match the repo's choice + +## Lint Command + +Use the repo's lint script first. Otherwise: + +- `cargo fmt --all -- --check` (CI), `cargo fmt` (apply) +- `cargo clippy --all-targets --all-features -- -D warnings` +- If `clippy.toml` / `rustfmt.toml` exists, the project has opinions — never override them in your tests + +## Project Layout + +``` +my_crate/ +├── Cargo.toml +├── src/ +│ ├── lib.rs # library crate root +│ ├── main.rs # binary crate root (mutually OK with lib.rs) +│ └── module.rs # private/public module +├── tests/ # integration tests — each .rs is a separate crate +│ └── widget.rs +├── benches/ # cargo bench targets +└── examples/ # cargo run --example name +``` + +| Test type | Where | Sees | +|-----------|-------|------| +| Unit test | `#[cfg(test)] mod tests` inside the source file | Private items in the surrounding module | +| Integration test | `tests/.rs` | Only the public API of the crate | +| Doc test | `///` doctests in source comments | Only the public API; runs via `cargo test --doc` | + +- **Unit tests** at the bottom of `module.rs`: + + ```rust + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn name_scenario_expected() { + // ... + } + } + ``` + +- **Integration tests** import the crate by name: `use my_crate::PublicType;` +- Helpers shared between integration tests must live in `tests/common/mod.rs` (the `mod.rs` form prevents cargo from treating them as a top-level test crate) + +## Test Function Patterns + +| Kind | Attribute | +|------|-----------| +| Sync test | `#[test]` | +| Should panic | `#[test] #[should_panic(expected = "message substring")]` | +| Ignored (long/manual) | `#[test] #[ignore = "reason"]` | +| Async test (Tokio) | `#[tokio::test]` (or `#[tokio::test(flavor = "multi_thread")]`) | +| Async test (async-std) | `#[async_std::test]` | +| Returning `Result` | `fn name() -> Result<(), Box>` — use `?` instead of `.unwrap()` | + +Pick the async harness the repo already uses. Do not mix `tokio` and `async-std` in tests. + +## Common Errors + +| Error | Fix | +|-------|-----| +| `cannot find type X in this scope` | Add `use crate::module::X;` or `use super::*;` inside the test module | +| `function or associated item not found in 'X'` | Verify the method exists on the exact type; check trait imports (e.g. `use std::io::Read`) | +| `the trait bound 'X: Y' is not satisfied` | Either implement the trait, add a `where` bound, or change the test to use a type that already implements it | +| `borrow of moved value` | Add `.clone()`, borrow with `&`, or restructure ownership — do not use `mem::transmute` to dodge it | +| `cannot borrow as mutable` | Make the binding `let mut x` or restructure to avoid simultaneous mutable + immutable borrows | +| `lifetime may not live long enough` | Add explicit lifetime annotations or use owned types (`String` instead of `&str`) in the test | +| `mismatched types` between `i32` and `usize` | Use `as` casts deliberately or change the literal type with a suffix (`5usize`, `5u32`) | +| `unresolved import 'crate::...'` in `tests/foo.rs` | Integration tests must import via the **crate name** (as listed in `Cargo.toml`), not `crate::` | +| `error: no test target found` for `cargo test --test foo` | The file must live directly in `tests/`, not `tests/subdir/foo.rs` (subdirs are treated as helpers) | +| `attempt to subtract with overflow` (debug) | Underflow on unsigned types; use `checked_sub`/`saturating_sub` or compare before subtracting | +| Doctest fails to compile | Use a leading "# " on hidden setup lines; mark code blocks `ignore`/`no_run`/`should_panic` if needed | +| `the following imports are unused` (warning treated as error) | Remove unused `use` statements; do not silence with `#[allow(unused_imports)]` | + +## Mocking Rules + +Rust has no single dominant mocking framework. Match the repo: + +- **Trait + struct fakes** (most idiomatic): define a trait, pass `Arc` or generic `T: Trait`, implement a fake struct in tests +- **`mockall`** crate: `#[automock]` on a trait generates `MockTrait` for use in tests +- **`mockito`** / **`wiremock`**: HTTP server mocks for client tests +- **`tempfile`**: scoped temp directories that auto-clean (`tempfile::tempdir()`) + +Avoid `unsafe` patches to "mock" free functions. Refactor to inject a trait instead. If a test needs more than 3 mocks, flag it as a design smell. + +## Features and `cfg` + +- Tests behind a feature flag run only when that feature is enabled — use `#[cfg(feature = "foo")]` on the `mod tests` or individual `#[test]` functions +- `--all-features` exercises everything but may pull conflicting features in some workspaces; check `cargo test --all-features` is part of CI before relying on it +- Use `#[cfg(test)]` to gate test-only helpers in production source files — not `#[cfg(feature = "test")]` + +## Concurrency, IO, and `unsafe` + +- Tests run in parallel by default. If your tests share global state (env vars, current dir, statics), serialize them with the `serial_test` crate (if present) or move state into the test +- Never write to `/tmp` or the repo dir directly — use `tempfile::tempdir()` so cleanup is automatic +- Tests in `unsafe` code should also run under Miri (`cargo +nightly miri test`) if the repo's CI does + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing: + +```toml +[dev-dependencies] +mockall = "0.12" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +``` + +Or via cargo: + +``` +cargo add --dev mockall +cargo add --dev tokio --features macros,rt-multi-thread +``` + +Match the major version of any tokio/serde/etc. already pinned by the workspace. + +## Skip Coverage Tools + +Do not configure or run coverage tools (`cargo tarpaulin`, `cargo llvm-cov`, `grcov`). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/swift.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/swift.md new file mode 100644 index 0000000..e4bf7b9 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/swift.md @@ -0,0 +1,227 @@ +# Swift Extension + +Language-specific guidance for Swift test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find files in `Tests/` (SPM) or `*Tests/` groups (Xcode) and copy their style. Distinguish **XCTest** (`import XCTest`, classes inheriting `XCTestCase`) from **Swift Testing** (`import Testing`, free functions tagged `@Test`) +2. **Project file** — `Package.swift` (SPM), `*.xcodeproj`, `*.xcworkspace`, or `Project.swift` (Tuist) +3. **Swift toolchain** — `.swift-version`, `swift-tools-version` line in `Package.swift`, `IPHONEOS_DEPLOYMENT_TARGET` and `SWIFT_VERSION` build settings in Xcode +4. **CI scripts** — `.github/workflows/*.yml`, `Fastfile`, `Makefile` — these reveal the canonical build/test invocation + +Use the testing framework the repo already uses. Both XCTest and Swift Testing can coexist in one target — match what the file you're adding tests next to uses. + +## Project Type Detection + +| Indicator | Project type | Build tool | +|-----------|--------------|------------| +| `Package.swift` only | Swift Package Manager | `swift build` / `swift test` | +| `*.xcodeproj` or `*.xcworkspace` | Xcode project (often app/iOS) | `xcodebuild` | +| Both | SPM library + Xcode app shell | Use SPM for library targets, Xcode for app targets | +| `Project.swift` (Tuist) | Tuist-generated Xcode project | Run `tuist generate` first, then xcodebuild | +| `project.yml` (XcodeGen) | XcodeGen-generated project | Run `xcodegen generate` first | + +If both an `.xcodeproj` and `.xcworkspace` exist (e.g. CocoaPods), **always pass `-workspace` not `-project`** to xcodebuild. + +## Build Commands + +### Swift Package Manager + +| Scope | Command | +|-------|---------| +| Build all | `swift build` | +| Build a target | `swift build --target MyLibrary` | +| Build for release | `swift build -c release` | + +### Xcode (`xcodebuild`) + +``` +xcodebuild build \ + -workspace MyApp.xcworkspace \ + -scheme MyAppScheme \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -configuration Debug +``` + +- Always specify `-destination` for iOS/tvOS/watchOS — the default may not exist on the build machine +- Use `-quiet` to suppress xcodebuild's chatty output, and pipe to `xcbeautify`/`xcpretty` if installed +- For deterministic CI builds add `-derivedDataPath ./DerivedData` + +## Test Commands + +### Swift Package Manager + +| Scope | Command | +|-------|---------| +| All tests | `swift test` | +| Filter by test name (XCTest) | `swift test --filter MyClassTests/testFooBar` | +| Filter by test name (Swift Testing) | `swift test --filter MyTestSuite.fooBar` | +| Parallel | `swift test --parallel` | +| Single platform | `swift test --triple x86_64-apple-macosx` (rare; usually skip) | + +### Xcode + +``` +xcodebuild test \ + -workspace MyApp.xcworkspace \ + -scheme MyAppScheme \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -only-testing:MyAppTests/MyClassTests/testFooBar +``` + +- `-only-testing:` and `-skip-testing:` accept `Bundle/Class/Method` paths and may be repeated +- `xcodebuild test-without-building` skips compilation if you've already built +- For Swift Testing in Xcode 16+, use the same `-only-testing:` syntax — the runner handles both frameworks + +## Lint Command + +Use the repo's lint tooling first: + +- `swiftlint lint --quiet` (autocorrect: `swiftlint --fix`) when `.swiftlint.yml` is present +- `swiftformat .` when `.swiftformat` is present +- Some projects gate format on a build phase — running `xcodebuild` may already invoke it + +## Project Layout + +### SPM + +``` +Package.swift +Sources/ +└── MyLibrary/ + ├── Foo.swift + └── Bar.swift +Tests/ +└── MyLibraryTests/ + └── FooTests.swift +``` + +- Test target name conventionally is `Tests` and lives in `Tests/Tests/` +- Test target must list its production target as a dependency in `Package.swift`: + + ```swift + .testTarget( + name: "MyLibraryTests", + dependencies: ["MyLibrary"]), + ``` + +### Xcode + +- Tests live in a separate target (e.g. `MyAppTests`) added to the scheme's "Test" action +- The test target's "Host Application" determines whether tests run on the simulator with the app loaded (unit tests) or as a UI test runner + +## Imports + +- XCTest: `import XCTest` plus `@testable import MyLibrary` to access `internal` symbols +- Swift Testing: `import Testing` plus `@testable import MyLibrary` +- `@testable` works only when the production target is built with `-enable-testing` (the SPM test target and Xcode "Debug" config do this by default) +- Never mark production code `public` solely to make it visible to tests — use `@testable import` instead + +## Test File Templates + +### Swift Testing (Xcode 16 / Swift 6) + +```swift +import Testing +@testable import MyLibrary + +@Suite("Calculator") +struct CalculatorTests { + @Test("add returns the sum of two integers") + func addReturnsSum() { + let calc = Calculator() + #expect(calc.add(2, 3) == 5) + } + + @Test("add throws on overflow", arguments: [ + (Int.max, 1), + (Int.min, -1), + ]) + func addThrowsOnOverflow(a: Int, b: Int) { + #expect(throws: ArithmeticError.self) { + try Calculator().add(a, b) + } + } +} +``` + +### XCTest + +```swift +import XCTest +@testable import MyLibrary + +final class CalculatorTests: XCTestCase { + func testAddReturnsSum() { + let calc = Calculator() + XCTAssertEqual(calc.add(2, 3), 5) + } + + func testAddThrowsOnOverflow() { + XCTAssertThrowsError(try Calculator().add(.max, 1)) { error in + XCTAssertEqual(error as? ArithmeticError, .overflow) + } + } +} +``` + +- XCTest requires test methods to start with `test` and take no arguments +- Mark XCTest classes `final` to silence warnings and prevent unintended subclassing +- Use `XCTUnwrap` instead of force-unwrapping (`!`) inside tests so the failure is reported rather than crashing the runner + +## Async, Throws, and Concurrency + +- Test methods may be `async` and/or `throws` in both frameworks +- For asynchronous expectations under XCTest, use `XCTestExpectation` + `wait(for:timeout:)` only when you cannot refactor to `async` +- For Swift Testing, use `await confirmation { ... }` to assert that a callback fires +- Cancel tasks deliberately with `Task.cancel()` instead of relying on test timeout + +## Common Errors + +| Error | Fix | +|-------|-----| +| `cannot find 'X' in scope` from a test | Add `@testable import MyLibrary` (and ensure the test target depends on it) | +| `module 'MyLibrary' was not compiled for testing` | Build the production target with `-enable-testing`; SPM test targets do this automatically — Xcode Debug configs need "Enable Testability" = YES | +| `failed to launch test runner` (Xcode) | Simulator destination may be invalid; list with `xcrun simctl list devices` and pick an existing one | +| `No such module 'XCTest'` outside a test target | XCTest is only available in test targets — do not import it from production code | +| `Static method 'expect(_:_:sourceLocation:)' is unavailable` / `No such module 'Testing'` | Swift Testing requires Swift 6 / Xcode 16+. On older toolchains, fall back to XCTest | +| `Symbol not found: _OBJC_CLASS_$_...` | Linker missing a framework; add it to the test target's "Link Binary With Libraries" | +| `signal SIGABRT` in tests | Often a force-unwrap on `nil`; replace `!` with `XCTUnwrap` to localize the failure | +| `MainActor-isolated property cannot be referenced from a non-isolated context` | Mark the test method `@MainActor` or move setup into a `MainActor` task | +| `Sandbox: ... deny file-write-create` | Use `FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)` instead of writing to fixed paths | +| Test discovery shows zero tests on Linux | XCTest on Linux needs `XCTMain([testCase(MyTests.allTests), ...])` in `Tests/LinuxMain.swift` (legacy SwiftPM only); for Swift 5.4+ this is auto-generated | + +## Mocking Rules + +Swift has no Mockito-equivalent — favor protocol-oriented design: + +- Define a **protocol** for the dependency, pass it via initializer, and implement a fake/stub struct in the test target +- For URL/HTTP, use `URLProtocol` subclasses to intercept `URLSession` requests, or use `MockingbirdSwift` / `Cuckoo` if the repo already adopts them +- For dates/clocks, inject a `Clock` (`ContinuousClock`, `SuspendingClock`, or a custom `Clock`-conforming type) — do not call `Date()` directly in business logic +- Avoid `swizzling` and runtime hacks — they break under Swift's optimizer + +If a test needs more than 3 mocks, flag it as a design smell. + +## Cross-Platform Considerations + +- Swift on Linux supports XCTest but **not** all of Foundation — guard with `#if canImport(Darwin)` or `#if os(macOS)` only when necessary +- Use `String(decoding:as:)` rather than `String(contentsOf:encoding:)` for cross-platform reads +- Be careful with `Bundle.main` in tests — on macOS unit tests it points to `xctest`, not your bundle; use `Bundle(for: type(of: self))` (XCTest) or a resource-bundle helper + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing. + +`Package.swift`: + +```swift +.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), +``` + +Then add to the test target's `dependencies:`. For CocoaPods/Carthage, edit `Podfile`/`Cartfile` and run `pod install` / `carthage update --use-xcframeworks`. + +## Skip Coverage Tools + +Do not configure or run coverage tools (`-enableCodeCoverage YES`, `xccov`, `slather`). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/typescript.md b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/typescript.md new file mode 100644 index 0000000..de26a70 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/code-testing-extensions/extensions/typescript.md @@ -0,0 +1,136 @@ +# TypeScript Extension + +Language-specific guidance for TypeScript (and JavaScript) test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*.test.ts` / `*.spec.ts` files and copy their style (imports, describe/it vs test, assertion patterns, mock approach) +2. **`package.json`** — `scripts.test`, `devDependencies`, `type` field +3. **Config files** — `tsconfig.json`, `jest.config.*`, `vitest.config.*`, `eslint.config.*` + +Use the repo's existing test runner and conventions — do not switch frameworks. If multiple runners are configured, follow whichever `scripts.test` invokes. Only introduce a framework if the repo has no tests at all. + +## Package Manager Detection + +Detect the package manager from lockfiles and use it consistently for **all** commands: + +| Indicator | Manager | Run script | Execute binary | +|-----------|---------|------------|----------------| +| `pnpm-lock.yaml` | pnpm | `pnpm test` | `pnpm exec ` | +| `yarn.lock` | Yarn | `yarn test` | `yarn ` | +| `bun.lockb` / `bun.lock` | Bun | `bun test` | `bunx ` | +| `package-lock.json` or none | npm | `npm test` | `npx ` | + +Use `` below as shorthand for the detected exec command. + +## Build Commands + +| Scope | Command | +|-------|---------| +| Type check | ` tsc --noEmit` or the repo's `typecheck` script | +| Build (if configured) | The repo's `build` script | + +Many projects don't need an explicit build step — the test runner handles transpilation. + +## Test Commands + +Detect the runner from `devDependencies` and `scripts.test`. Always prefer the repo's test script first. + +| Runner | Run once | Filter by file | Filter by name | +|--------|----------|----------------|----------------| +| **Jest** | ` jest` | ` jest path/to/file` | ` jest -t "name"` | +| **Vitest** | ` vitest run` | ` vitest run path/to/file` | ` vitest run -t "name"` | +| **Mocha** | ` mocha` | (use config or positional args) | ` mocha --grep "name"` | + +- **Always use `vitest run`** (not bare `vitest`) — bare `vitest` starts watch mode +- **Never use `--watch`** — the agent must not start interactive/watch mode +- For Jest: `--bail` to stop on first failure, `--verbose` for detail +- Mocha `--grep` filters by **test name**, not file path + +## Lint Command + +Use the repo's lint script first. Otherwise detect from `devDependencies` and config: + +- `eslint.config.*` or `.eslintrc.*` → ` eslint --fix path/to/file.ts` +- `prettier` → ` prettier --write path/to/file.ts` +- `biome.json` → ` biome check --write path/to/file.ts` + +## Project Layout and Imports + +| Layout | Import Style | +|--------|-------------| +| Colocated (`src/module.test.ts`) | `import { X } from './module'` | +| `__tests__/` dir | `import { X } from '../module'` | +| Top-level `tests/` | `import { X } from '../src/module'` | + +- **Match existing test imports** — copy path style from neighboring tests +- If `tsconfig.json` has `paths` aliases (e.g., `@/`), use them in tests too +- For monorepos: import from the package name, not relative cross-package paths +- For monorepo workspaces (Nx, Turborepo, Lerna): run tests via the workspace tool (`nx test `, `turbo test`), not from a random package directory + +## Test File Naming + +- Match existing convention — check for `.test.ts` vs `.spec.ts` +- Jest/Vitest default: `*.test.ts`, `*.spec.ts`, or files inside `__tests__/` +- Place test files to mirror the existing project pattern + +## Common Errors + +| Error | Fix | +|-------|-----| +| `Cannot find module 'X'` | Check existing imports for correct paths; verify `tsconfig.json` `paths`; check `moduleNameMapper` (Jest) or `resolve.alias` (Vitest) | +| `TS2305: has no exported member` | Verify the exact export name from the source file | +| `TS2345: type not assignable` | Match the expected type; use type assertion only for mock objects | +| `SyntaxError: Unexpected token` / `Jest encountered an unexpected token` | Verify TS transform config (`ts-jest`, `@swc/jest`, or Vitest handles natively) | +| `ReferenceError: describe is not defined` | Vitest: import from `vitest` or set `globals: true` in config; Jest: ensure tests run under Jest not bare `node` | +| `Cannot use import statement outside a module` / `ERR_REQUIRE_ESM` | ESM/CJS mismatch — align runner config with the project's module system (see ESM section); do **not** blindly set `"type": "module"` | +| `ReferenceError: document is not defined` | Set test environment: `testEnvironment: 'jsdom'` (Jest) or `environment: 'jsdom'` (Vitest) | +| `jest.mock() ... out-of-scope variables` | Keep `jest.mock()` at top level; don't reference variables declared after the mock call (Jest hoists mocks) | +| `Cannot find module '@/...'` | Mirror the project's alias config in the test runner's module resolution | +| `Warning: not wrapped in act(...)` | Await async UI updates using the repo's existing pattern (`waitFor`, `act`) | + +## ESM vs CommonJS + +Check these signals to determine the project's module system: + +- `"type": "module"` in `package.json` → ESM +- `"module": "ESNext"` or `"NodeNext"` in `tsconfig.json` → ESM output (but not sufficient alone) +- `.mjs`/`.mts` extensions → ESM files + +If the test runner fails with ESM errors, align the runner's config with the project's module system. **Do not change `package.json` `type` field** — align the test runner to match whatever the project uses: + +- **Jest**: `--experimental-vm-modules` + `ts-jest` with `useESM: true`, or `@swc/jest` +- **Vitest**: handles ESM natively +- **Mocha**: `--loader ts-node/esm` + +## Mocking Rules + +- Prefer dependency injection over module mocking +- Use typed mocks: `jest.Mocked`, `vi.mocked(obj)`, or `Partial` with `as T` +- Jest: `jest.mock()` is hoisted — keep at top level, don't close over local variables +- Vitest: `vi.mock()` follows the same hoisting rules +- If a test needs more than 3–4 mocks, flag it as a design smell +- Mock reset: rely on `clearMocks`/`restoreMocks` config if present; otherwise reset in `beforeEach` + +## Framework-Specific Notes + +- **React/Preact**: use `@testing-library/react`, wrap with necessary providers (router, query client, theme) matching existing test setup +- **Express/Koa**: use `supertest` for HTTP testing if the repo already uses it +- **NestJS**: build testing module with `Test.createTestingModule` — don't instantiate controllers directly + +## Dependency Installation (Last Resort) + +Only install packages after investigation confirms they are missing. Use the detected package manager: + +``` + add --save-dev jest ts-jest @types/jest + add --save-dev vitest +``` + +Never install test infrastructure that conflicts with what the repo already uses. + +## Skip Coverage Tools + +Do not configure or run coverage tools (istanbul, c8, `vitest --coverage`). Coverage is measured separately by the evaluation harness. diff --git a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/SKILL.md index 1bfbff3..128f2bd 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/SKILL.md @@ -1,20 +1,17 @@ --- name: coverage-analysis description: > - Automated, project-wide code coverage and CRAP (Change Risk Anti-Patterns) - score analysis for .NET projects with existing unit tests. Auto-detects - solution structure, runs coverage collection via `dotnet test` (supports both - Microsoft.Testing.Extensions.CodeCoverage and Coverlet), generates reports via - ReportGenerator, calculates CRAP scores per method, and surfaces risk - hotspots — complex code with low test coverage that is dangerous to modify. - Use when the user wants project-wide coverage analysis with risk - prioritization, coverage gap identification, CRAP score computation - across an entire solution, or to diagnose why coverage is stuck or - plateaued and identify what methods are blocking improvement. - DO NOT USE FOR: targeted single-method CRAP analysis (use crap-score skill), - writing tests, running tests without coverage collection, applying test - filters, producing TRX reports, or troubleshooting test execution (use - run-tests for all of these). + Project-wide code coverage and CRAP (Change Risk Anti-Patterns) score + analysis for .NET projects. Calculates CRAP scores per method and surfaces + risk hotspots — complex code with low coverage that is dangerous to modify. + Use to diagnose why coverage is stuck or plateaued, identify what methods + block improvement, or get project-wide coverage analysis with risk ranking. + USE FOR: coverage stuck, coverage plateau, can't increase coverage, what's + blocking coverage, coverage gap, CRAP scores, risk hotspots, where to add + tests, coverage analysis, coverage report. + DO NOT USE FOR: targeted single-method CRAP analysis (use crap-score), + writing tests, running tests without coverage, or troubleshooting test + execution (use run-tests). license: MIT --- @@ -55,8 +52,9 @@ Use this skill when the user mentions test coverage, coverage gaps, code risk, C ### Prerequisites - .NET SDK installed (`dotnet` on PATH) -- At least one test project referencing the production code (xUnit, NUnit, or MSTest) -- Internet access for `dotnet tool install` (ReportGenerator) on first run, or ReportGenerator already installed globally +- At least one test project referencing the production code (xUnit, NUnit, or MSTest) — only required for the from-scratch path; not needed when the user supplies an existing Cobertura XML +- **Optional, only for the from-scratch path:** internet/NuGet access for `dotnet add package coverlet.collector` (or `Microsoft.Testing.Extensions.CodeCoverage`) when a test project has no coverage provider yet. Skip when the user supplies an existing Cobertura XML. +- **Optional, only for Phase 5:** internet access for `dotnet tool install` (ReportGenerator). Core CRAP/coverage analysis works from Cobertura XML alone — ReportGenerator only adds HTML/CSV reports as an optional post-summary extra. The skill auto-detects coverage provider state per test project and selects the least-invasive execution strategy: @@ -68,9 +66,13 @@ No pre-existing runsettings files or manually installed tools required. ## Workflow -If the user provides a path to existing Cobertura XML (or coverage data is already present in `TestResults/`), skip Steps 3–4 (test execution and provider detection) but **still run Steps 5–6** (ReportGenerator and CRAP score computation). The Risk Hotspots table and CRAP scores are mandatory in every output — they are the skill's core value-add over raw coverage numbers. +> **MANDATORY: deliver the final assistant response with the CRAP/risk-hotspot summary BEFORE any optional work.** As soon as `Compute-CrapScores.ps1` and `Extract-MethodCoverage.ps1` return data, your **next** assistant response must contain the user-facing analysis (CRAP table, blocking methods, recommendations). Do not run ReportGenerator (Phase 5), do not install global tools, and do not start any heavy parallel work before that response is delivered. The user is judged on the final assistant message, not on side-effect files. +> +> If a phase fails, times out, or budget is running low, skip remaining optional work and immediately return a partial summary containing: (1) what was found in the Cobertura XML, (2) any CRAP/risk-hotspot data already extracted, (3) which methods are blocking coverage, and (4) failures encountered. -The workflow runs in four phases. Phases 2 and 3 each contain steps that can run in parallel to reduce total wall-clock time. +If the user provides a path to existing Cobertura XML (or coverage data is already present in `TestResults/`), **skip Phase 2 entirely** (no test execution) **and skip Phase 5 by default** (no ReportGenerator install or HTML report) — go directly from Phase 3 (analysis scripts) to Phase 4 (user-facing summary). Only run Phase 5 if the user explicitly asks for HTML/CSV reports. The Risk Hotspots table and CRAP scores are mandatory in every output — they are the skill's core value-add over raw coverage numbers. + +The workflow runs in five phases. Phases 1–4 are required; Phase 5 (ReportGenerator HTML/CSV reports) is strictly optional and runs **after** the user-facing summary has been delivered. Do not parallelize Phase 5 with earlier phases — the heavy `dotnet tool install` for ReportGenerator can crash the session before Phase 4 completes. ### Phase 1 — Setup (sequential) @@ -131,7 +133,13 @@ Write-Host "TEST_PROJECTS:$($testProjects.Count)" $testProjects | ForEach-Object { Write-Host "TEST_PROJECT:$($_.FullName)" } # Resolve the test output root (where coverage-analysis artifacts will be written) -if ($testProjects.Count -eq 1) { +if ($testProjects.Count -eq 0) { + if ($gitRoot) { + $testOutputRoot = $gitRoot + } else { + $testOutputRoot = $root + } +} elseif ($testProjects.Count -eq 1) { $testOutputRoot = $testProjects[0].DirectoryName } else { # Multiple test projects — find their deepest common parent directory @@ -165,7 +173,8 @@ Write-Host "TEST_OUTPUT_ROOT:$testOutputRoot" - If `ENTRY_TYPE:NotFound` and test projects were found → use the test projects directly as entry points (run `dotnet test` on each test `.csproj`). - If `ENTRY_TYPE:NotFound` and no test projects found → stop: `No .sln or test projects found under . Provide the path to your .NET solution or project.` -- If `TEST_PROJECTS:0` → stop: `No test projects found (expected projects with 'Test' or 'Spec' in the name). Ensure your solution has unit test projects before running coverage analysis.` +- If `TEST_PROJECTS:0` and `EXISTING_COBERTURA_COUNT` > 0 (Step 2b) → continue with existing Cobertura XML analysis (no `dotnet test` run). +- If `TEST_PROJECTS:0` and `EXISTING_COBERTURA_COUNT` == 0 → stop: `No test projects found (expected projects with 'Test' or 'Spec' in the name), and no existing Cobertura XML was provided. Add a test project or provide a Cobertura file path.` #### Step 2: Create the output directory @@ -176,7 +185,40 @@ New-Item -ItemType Directory -Path $coverageDir -Force | Out-Null Write-Host "COVERAGE_DIR:$coverageDir" ``` -#### Step 2b: Recommend ignoring `TestResults/` +This step only manages the `TestResults/coverage-analysis/` subdirectory (skill-owned outputs). It must never delete user-supplied Cobertura files — those live one level up at `TestResults/coverage.cobertura.xml` (or wherever the user pointed). If the user provided a path that *is* `TestResults/coverage-analysis/...`, copy the file aside before this step recreates the directory. + +#### Step 2b: Discover or accept existing Cobertura XML (required for the existing-data path) + +If the user supplied a Cobertura XML path explicitly, use it. Otherwise probe well-known locations and any path the user mentioned: + +```powershell +# 1. Honor a user-supplied path first (highest priority) +$coberturaFiles = @() +if ($userSuppliedCoberturaPath -and (Test-Path $userSuppliedCoberturaPath)) { + $coberturaFiles = @(Get-Item $userSuppliedCoberturaPath) +} + +# 2. Otherwise scan TestResults/ at the repo/test root for any *.cobertura.xml +if ($coberturaFiles.Count -eq 0) { + $searchPaths = @( + (Join-Path $testOutputRoot "TestResults"), + (Join-Path $root "TestResults") + ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique + foreach ($sp in $searchPaths) { + $found = @(Get-ChildItem -Path $sp -Filter "*.cobertura.xml" -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[/\\]coverage-analysis[/\\]raw[/\\]' }) + if ($found.Count -gt 0) { $coberturaFiles = $found; break } + } +} + +Write-Host "EXISTING_COBERTURA_COUNT:$($coberturaFiles.Count)" +$coberturaFiles | ForEach-Object { Write-Host "EXISTING_COBERTURA:$($_.FullName)" } +``` + +- If `EXISTING_COBERTURA_COUNT` > 0 → **skip Phase 2 entirely** and pass these paths to the Phase 3 scripts. +- If `EXISTING_COBERTURA_COUNT` == 0 → run Phase 2 to generate fresh coverage; the file paths to feed Phase 3 will be discovered from `/raw/` after `dotnet test`. + +#### Step 2c: Recommend ignoring `TestResults/` ```powershell $pattern = "**/TestResults/" @@ -198,9 +240,9 @@ if ($gitRoot) { } ``` -### Phase 2 — Data collection (Steps 3 and 4 run in parallel) +### Phase 2 — Test execution (skip when Cobertura XML already exists) -Steps 3 and 4 are independent — start both simultaneously. `dotnet test` is the slowest step, and ReportGenerator setup doesn't need coverage files, so running them concurrently cuts wall time significantly. +Run only when no Cobertura XML is present. If the user already has coverage data, skip directly to Phase 3. #### Step 3: Detect coverage provider and run `dotnet test` with coverage collection @@ -361,7 +403,67 @@ If `COBERTURA_COUNT` is 0: - If `VS_BINARY_COVERAGE` > 0: warn the user — *"Found .coverage files (VS binary format) but no Cobertura XML. These were likely produced by Visual Studio's built-in collector, which outputs a binary format by default. This skill needs Cobertura XML. Re-running with the detected provider configured for Cobertura output."* Then re-run the appropriate `dotnet test` command above (Coverlet or Microsoft CodeCoverage) with Cobertura format. - If no `.coverage` files either: stop and report — *"Coverage files not generated. Ensure `dotnet test` completed successfully and check the build output for errors."* -#### Step 4: Verify or install ReportGenerator (parallel with Step 3) +### Phase 3 — Analysis (sequential) + +Run the two bundled PowerShell scripts. Both are cheap and complete in seconds. **Do not** install or invoke ReportGenerator here — that belongs in optional Phase 5, after the user-facing summary has been delivered. + +#### Step 4: Calculate CRAP scores using the bundled script + +Run `scripts/Compute-CrapScores.ps1` (co-located with this SKILL.md). It reads all Cobertura XML files, applies `CRAP(m) = comp² × (1 − cov)³ + comp` per method, and returns the top-N hotspots as JSON. + +To locate the script: find the directory containing this skill's `SKILL.md` file (the skill loader provides this context), then resolve `scripts/Compute-CrapScores.ps1` relative to it. If the script path cannot be determined, calculate CRAP scores inline using the formula below. + +```powershell +& "/scripts/Compute-CrapScores.ps1" ` + -CoberturaPath @() ` + -CrapThreshold ` + -TopN +``` + +Script outputs: `OVERALL_LINE_COVERAGE:`, `OVERALL_BRANCH_COVERAGE:` (aggregated project-wide rates across all provided Cobertura files), `TOTAL_METHODS:`, `FLAGGED_METHODS:`, `HOTSPOTS:` (top-N sorted by CrapScore descending). The OVERALL_* values are exactly what the Phase 4 summary needs for the "Line Coverage" / "Branch Coverage" rows — no separate XML parsing tool call is required. + +#### Step 5: Extract per-method coverage gaps + +Run `scripts/Extract-MethodCoverage.ps1` to get per-method coverage data for the Coverage Gaps table: + +```powershell +& "/scripts/Extract-MethodCoverage.ps1" ` + -CoberturaPath @() ` + -CoverageThreshold ` + -BranchThreshold ` + -Filter below-threshold +``` + +Script outputs: JSON array of methods below the coverage threshold, sorted by coverage ascending. Use this data to populate the Coverage Gaps by File table in the report. + +### Phase 4 — User-facing summary (MANDATORY — your next assistant response) + +As soon as Phase 3 completes, **your immediately next assistant response must contain the user-facing analysis** — do not interleave any other tool calls before it. This is the response the user (and any judge) sees. Skipping or deferring this in favor of Phase 5 (ReportGenerator) is a hard failure. + +The response must include, at minimum: + +1. Overall line and branch coverage — read directly from the `OVERALL_LINE_COVERAGE:` / `OVERALL_BRANCH_COVERAGE:` lines emitted by `Compute-CrapScores.ps1` (no extra Cobertura parsing required) +2. The Risk Hotspots table built from `Compute-CrapScores.ps1` `HOTSPOTS:` output (CRAP scores, complexity, coverage) +3. Identification of the highest-risk method(s) and what is blocking coverage +4. 1–3 prioritized, specific recommendations (which method to test, expected CRAP/coverage impact) + +Use `references/output-format.md` verbatim for fixed headings, table structures, symbols, and emoji. Use `references/guidelines.md` for prioritization rules and style. + +If Phase 5 has not yet run when you compose this summary, mark the `## 📁 Reports` section's HTML/Text/CSV/GitHub-markdown rows as `Not generated (optional — request HTML reports to enable)`. Only the `coverage-analysis.md` and raw Cobertura paths are guaranteed to exist. + +Attempt to save the same content to `TestResults/coverage-analysis/coverage-analysis.md` before delivering the response (use the editor's create/edit tool — do not shell out). If the file write fails, still deliver the summary and note the file-write failure explicitly. + +### Phase 5 — Optional: ReportGenerator HTML/CSV reports (post-summary) + +Phase 5 is **strictly optional** and runs **only after** Phase 4 has been delivered. Skip Phase 5 entirely when: + +- The user supplied existing Cobertura XML and only asked for analysis (the default for the existing-data path). +- The user is diagnosing a coverage plateau or asking "what's blocking me?" — they want the answer, not a static-site report. +- ReportGenerator is not already installed and you have no clear signal the user wants HTML reports. + +Run Phase 5 only when the user explicitly asks for HTML/CSV reports, or when the project flow requires them (e.g., a CI artifact upload step). + +#### Step 6: Verify or install ReportGenerator (only if running Phase 5) ```powershell $rgAvailable = $false @@ -390,13 +492,9 @@ if ($rgCommand) { Write-Host "RG_AVAILABLE:$rgAvailable" ``` -If installation fails (no internet), keep `RG_AVAILABLE:false` and continue with raw Cobertura XML parsing + script-based analysis in Step 6. Skip HTML/Text/CSV report generation in Step 5 and note this in the output. +If installation fails (no internet), keep `RG_AVAILABLE:false`, leave the existing user-facing summary as the final output, and note that HTML reports were skipped. -### Phase 3 — Analysis (Steps 5 and 6 run in parallel) - -Once Phase 2 completes (coverage files available, ReportGenerator ready), start Steps 5 and 6 simultaneously — both read from the same Cobertura XML and produce independent outputs. - -#### Step 5: Generate reports with ReportGenerator (parallel with Step 6) +#### Step 7: Generate HTML/CSV reports ```powershell $reportsDir = Join-Path "" "reports" @@ -414,60 +512,21 @@ if ($rgAvailable) { } ``` -#### Step 6: Calculate CRAP scores using the bundled script (parallel with Step 5) - -Run `scripts/Compute-CrapScores.ps1` (co-located with this SKILL.md). It reads all Cobertura XML files, applies `CRAP(m) = comp² × (1 − cov)³ + comp` per method, and returns the top-N hotspots as JSON. - -To locate the script: find the directory containing this skill's `SKILL.md` file (the skill loader provides this context), then resolve `scripts/Compute-CrapScores.ps1` relative to it. If the script path cannot be determined, calculate CRAP scores inline using the formula below. - -```powershell -& "/scripts/Compute-CrapScores.ps1" ` - -CoberturaPath @() ` - -CrapThreshold ` - -TopN -``` - -Script outputs: `TOTAL_METHODS:`, `FLAGGED_METHODS:`, `HOTSPOTS:` (top-N sorted by CrapScore descending). - -Also run `scripts/Extract-MethodCoverage.ps1` to get per-method coverage data for the Coverage Gaps table: - -```powershell -& "/scripts/Extract-MethodCoverage.ps1" ` - -CoberturaPath @() ` - -CoverageThreshold ` - -BranchThreshold ` - -Filter below-threshold -``` - -Script outputs: JSON array of methods below the coverage threshold, sorted by coverage ascending. Use this data to populate the Coverage Gaps by File table in the report. - -### Phase 4 — Output (sequential) - -#### Step 7: Build the output report - -Compose the analysis and save it to `TestResults/coverage-analysis/coverage-analysis.md` under the test project directory. Print the full report to the console. - -After saving the file, automatically open `TestResults/coverage-analysis/coverage-analysis.md` in the editor so the user can review it immediately. - -- In editor-hosted environments (VS Code, Visual Studio, or other IDE hosts): open the file in the current host session/editor context after writing it. -- Do not launch a different app instance via hardcoded shell commands (for example `code`, `start`, or platform-specific open commands) unless the host has no native open-file mechanism. -- In CLI or non-editor environments: print the absolute report path and clearly state that the file was generated. - -Do not ask for confirmation before opening the report file. - -Use `references/output-format.md` verbatim for all fixed headings, table structures, symbols, and emoji in the generated report. Use `references/guidelines.md` for execution constraints, prioritization rules, and style. +After Phase 5 completes successfully, you may follow up with a short message pointing the user to the generated HTML report (one paragraph, no need to repeat the summary). ## Validation -- Verify that at least one `coverage.cobertura.xml` file was generated after `dotnet test` +- Verify that at least one `coverage.cobertura.xml` file was generated after `dotnet test` (or already exists when the user supplied one) +- Confirm the assistant response contained the CRAP/risk-hotspot table — saving the markdown file is secondary - Confirm `TestResults/coverage-analysis/coverage-analysis.md` was written and contains data - Spot-check one method's CRAP score: `comp² × (1 − cov)³ + comp` — a method with 100% coverage should have CRAP = complexity -- If ReportGenerator ran, verify `TestResults/coverage-analysis/reports/index.html` exists +- If Phase 5 ran, verify `TestResults/coverage-analysis/reports/index.html` exists; otherwise the report file should mark HTML/Text/CSV rows as `Not generated` ## Common Pitfalls - **No Cobertura XML generated** — the test project may lack a coverage provider. The skill auto-adds one, but if `dotnet add package` fails (offline/proxy), coverage collection silently produces nothing. Check for `.coverage` binary files as a fallback indicator. - **Test failures (exit code 1)** — coverage is still collected from passing tests. Do not abort; proceed with partial data and note the failures in the summary. -- **ReportGenerator install failure** — if `dotnet tool install` fails (no internet), skip HTML/CSV report generation and continue with raw Cobertura XML analysis + script-based CRAP scores. Note the skip in the report. +- **Premature end before user-facing summary** — never start Phase 5 (ReportGenerator install/run) before the Phase 4 assistant response is delivered. The heavy `dotnet tool install` can crash the session or exhaust budget, leaving the user with no analysis even though the CRAP scores were already computed. +- **ReportGenerator install failure** — if `dotnet tool install` fails (no internet) during Phase 5, leave the existing Phase 4 summary as the final output and note that HTML reports were skipped. Do not retry or block on the install. - **Method name mismatches in Cobertura** — async methods, lambdas, and local functions may have compiler-generated names. The scripts use the Cobertura method name/signature directly; verify against source if results look unexpected. - **Mixed coverage providers** — when a solution contains both Coverlet and Microsoft CodeCoverage projects, the skill runs per-project to avoid dual-provider conflicts. This is slower but correct. diff --git a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/guidelines.md b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/guidelines.md index cb38224..344f69e 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/guidelines.md +++ b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/guidelines.md @@ -2,7 +2,7 @@ **Don't modify source or production code.** The only permitted project file modifications are adding a coverage provider package to test projects that currently have no provider: `coverlet.collector` (coverlet/mixed modes) or `Microsoft.Testing.Extensions.CodeCoverage` (ms-codecoverage mode). Do not add a second provider to projects that already have one. Always log package additions and document revert commands in the report. Write all other output to `TestResults/coverage-analysis/` under the test project directory. -**Always show and open the generated markdown report.** After writing `TestResults/coverage-analysis/coverage-analysis.md`, print its contents to the console and open the file in the current host editor/session automatically (when an editor is available). +**Always show and open the generated markdown report — but only after the assistant response with the CRAP/risk-hotspot summary has been delivered.** Saving and opening `TestResults/coverage-analysis/coverage-analysis.md` is a follow-up action; it must never delay the user-facing summary. **Don't generate new tests during the initial analysis run.** This skill surfaces where tests are needed. Test generation is a separate follow-up step outside the scope of this skill. diff --git a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/output-format.md b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/output-format.md index c768806..7e3c5b6 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/output-format.md +++ b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/references/output-format.md @@ -24,7 +24,8 @@ Copy the template below **verbatim** for all fixed elements (headings, table hea | **Test Result** | | — | ✅ / ⚠️ | > Coverage collected from ** of test project(s)**. -> Reports saved to: `/reports/` +> Outputs saved to: `/` (markdown summary + raw Cobertura XML). +> *If Phase 5 ran:* HTML/CSV reports also at `/reports/`. If any coverage provider package was added to test projects, include this note after the summary: @@ -75,9 +76,12 @@ Files below the line or branch coverage threshold, ordered by uncovered lines de | Report | Path | |--------|------| -| HTML (browsable) | `/reports/index.html` | -| Text summary | `/reports/Summary.txt` | -| GitHub markdown | `/reports/SummaryGithub.md` | -| CSV data | `/reports/Summary.csv` | -| Raw data | `/raw/` | +| Markdown summary (this file) | `/coverage-analysis.md` | +| Raw Cobertura XML | `` | +| HTML (browsable) | `/reports/index.html` *or* `Not generated (optional — request HTML reports to enable)` | +| Text summary | `/reports/Summary.txt` *or* `Not generated` | +| GitHub markdown | `/reports/SummaryGithub.md` *or* `Not generated` | +| CSV data | `/reports/Summary.csv` *or* `Not generated` | ``` + +If ReportGenerator (Phase 5) has not run, mark the HTML/Text/GitHub-markdown/CSV rows as `Not generated (optional — request HTML reports to enable)`. Do not invent paths for files that have not been produced. For **Raw Cobertura XML**, list the actual XML file path(s) used in analysis (for from-scratch runs this is typically under `/raw/`; for existing-data runs this may be under `TestResults/` or another user-supplied location). diff --git a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 index a4b1799..b0c8d9f 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +++ b/catalog/Testing/Official-DotNet-Test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 @@ -8,8 +8,11 @@ # .\Compute-CrapScores.ps1 -CoberturaPath ,,... [-CrapThreshold ] [-TopN ] # # Outputs: -# - Hotspot rows (top N by CRAP score) as a JSON array to stdout (HOTSPOTS:) -# - Summary counts as TOTAL_METHODS: and FLAGGED_METHODS: +# - OVERALL_LINE_COVERAGE: (aggregate line coverage across input files, as percent) +# - OVERALL_BRANCH_COVERAGE: (aggregate branch coverage across input files, as percent) +# - TOTAL_METHODS: +# - FLAGGED_METHODS: +# - HOTSPOTS: (top N by CRAP score) param( [Parameter(Mandatory)][string[]]$CoberturaPath, @@ -18,8 +21,16 @@ param( ) # Merge methods across all Cobertura files using a stable key (Class|Method|Signature|File). -# Line hits are accumulated so a line is counted as covered if any test project covered it. +# Line hits are accumulated so a line is counted as covered if any input coverage file covered it. $methodMap = @{} +$overallLineRate = 0.0 +$overallBranchRate = 0.0 +$totalLinesCovered = 0 +$totalLinesValid = 0 +$totalBranchesCovered = 0 +$totalBranchesValid = 0 +$fallbackLineRates = [System.Collections.Generic.List[double]]::new() +$fallbackBranchRates = [System.Collections.Generic.List[double]]::new() foreach ($filePath in $CoberturaPath) { if (-not (Test-Path $filePath)) { @@ -34,6 +45,20 @@ foreach ($filePath in $CoberturaPath) { exit 2 } + # Prefer aggregate numerator/denominator attributes when present. + if ($null -ne $cobertura.coverage.'lines-covered' -and $null -ne $cobertura.coverage.'lines-valid') { + $totalLinesCovered += [double]$cobertura.coverage.'lines-covered' + $totalLinesValid += [double]$cobertura.coverage.'lines-valid' + } elseif ($cobertura.coverage.'line-rate') { + $fallbackLineRates.Add([double]$cobertura.coverage.'line-rate') + } + if ($null -ne $cobertura.coverage.'branches-covered' -and $null -ne $cobertura.coverage.'branches-valid') { + $totalBranchesCovered += [double]$cobertura.coverage.'branches-covered' + $totalBranchesValid += [double]$cobertura.coverage.'branches-valid' + } elseif ($cobertura.coverage.'branch-rate') { + $fallbackBranchRates.Add([double]$cobertura.coverage.'branch-rate') + } + foreach ($package in $cobertura.coverage.packages.package) { foreach ($class in $package.classes.class) { $className = $class.name @@ -104,6 +129,33 @@ foreach ($entry in $methodMap.Values) { $hotspots = $results | Sort-Object CrapScore -Descending | Select-Object -First $TopN $flagged = $results | Where-Object { $_.CrapScore -gt $CrapThreshold } +if ($totalLinesValid -gt 0) { + $overallLineRate = $totalLinesCovered / $totalLinesValid +} else { + # Fallback approximation when Cobertura aggregate counters and per-file rates are unavailable. + # This uses merged method line totals and may under/over-estimate if Cobertura + # includes executable lines outside method nodes. + $mergedTotalLines = ($results | Measure-Object -Property TotalLines -Sum).Sum + $mergedCoveredLines = ($results | Measure-Object -Property CoveredLines -Sum).Sum + if ($mergedTotalLines -gt 0) { + $overallLineRate = [double]$mergedCoveredLines / [double]$mergedTotalLines + } elseif ($fallbackLineRates.Count -gt 0) { + $overallLineRate = ($fallbackLineRates | Measure-Object -Average).Average + } else { + $overallLineRate = 0.0 + } +} + +if ($totalBranchesValid -gt 0) { + $overallBranchRate = $totalBranchesCovered / $totalBranchesValid +} elseif ($fallbackBranchRates.Count -gt 0) { + $overallBranchRate = ($fallbackBranchRates | Measure-Object -Average).Average +} else { + $overallBranchRate = 0.0 +} + +Write-Host "OVERALL_LINE_COVERAGE:$([Math]::Round($overallLineRate * 100, 1))" +Write-Host "OVERALL_BRANCH_COVERAGE:$([Math]::Round($overallBranchRate * 100, 1))" Write-Host "TOTAL_METHODS:$($results.Count)" Write-Host "FLAGGED_METHODS:$($flagged.Count)" if ($hotspots) { diff --git a/catalog/Testing/Official-DotNet-Test/skills/crap-score/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/crap-score/SKILL.md index 3825905..352dba4 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/crap-score/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/crap-score/SKILL.md @@ -1,13 +1,14 @@ --- name: crap-score description: > - Calculates CRAP (Change Risk Anti-Patterns) score for .NET methods, classes, - or files. Use when the user asks to assess test quality, identify risky - untested code, compute CRAP scores, or evaluate whether complex methods have - sufficient test coverage. Requires code coverage data (Cobertura XML) and - cyclomatic complexity analysis. - DO NOT USE FOR: writing tests, general test execution unrelated to coverage/CRAP - analysis, or general code coverage reporting without CRAP context. + Calculates targeted CRAP (Change Risk Anti-Patterns) scores for a named .NET + method, class, or single source file. Use when the user explicitly asks to + compute CRAP scores or assess risky untested code for a specific target, + combining Cobertura coverage data with cyclomatic complexity analysis. + DO NOT USE FOR: project-wide coverage analysis, coverage plateau or "stuck + coverage" diagnosis, what's blocking coverage, or where to add tests across + a project (use coverage-analysis); writing tests; running tests without + CRAP context. license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/skills/detect-static-dependencies/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/detect-static-dependencies/SKILL.md index 385b612..46bda03 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/detect-static-dependencies/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/detect-static-dependencies/SKILL.md @@ -24,6 +24,12 @@ Scan a C# codebase for calls to hard-to-test static APIs and produce a ranked re - Prioritizing which statics to wrap first (highest-frequency wins) - Creating a migration plan for incremental testability improvements +## Response Guidelines + +- Scale the response to the user's request. A question about a specific category (e.g., "find time statics") should focus on that category with file locations and counts, not produce a full report across all categories. +- When the user provides a specific file or directory path, scan only that scope — do not expand to the entire solution unless asked. +- The full structured report format in Step 4 is for comprehensive audit requests. For focused questions, return only the relevant subset (e.g., category summary + affected files for the requested category). + ## When Not to Use - The user wants wrappers generated (hand off to `generate-testability-wrappers`) diff --git a/catalog/Testing/Official-DotNet-Test/skills/migrate-mstest-v1v2-to-v3/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/migrate-mstest-v1v2-to-v3/SKILL.md index 5667a0f..ca7fea4 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/migrate-mstest-v1v2-to-v3/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/migrate-mstest-v1v2-to-v3/SKILL.md @@ -34,7 +34,7 @@ Migrate a test project from MSTest v1 (assembly references) or MSTest v2 (NuGet ## When Not to Use -- Project already uses MSTest v3 (3.x packages) +- Project already on MSTest v3 with no migration-related build errors (fully migrated) - Upgrading v3 to v4 -- use `migrate-mstest-v3-to-v4` - Migrating between frameworks (MSTest to xUnit/NUnit) diff --git a/catalog/Testing/Official-DotNet-Test/skills/migrate-vstest-to-mtp/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/migrate-vstest-to-mtp/SKILL.md index 1e002d3..d1d8928 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/migrate-vstest-to-mtp/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/migrate-vstest-to-mtp/SKILL.md @@ -4,17 +4,17 @@ description: > Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", or mentions EnableMSTestRunner, - EnableNUnitRunner, UseMicrosoftTestingPlatformRunner, dotnet test exit - code 8, zero tests discovered, or MTP behavioral differences - (--ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE). + EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. + USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests + discovered), --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE. Supports MSTest, NUnit, xUnit.net v2 (via YTest.MTP.XUnit2), and xUnit.net v3 (native MTP). Covers runner enablement, CLI argument - translation, xUnit.net v3 filter syntax, Directory.Build.props and - global.json configuration, CI/CD pipeline updates, and MTP extension - packages. DO NOT USE FOR: migrating between test frameworks - (MSTest/xUnit/NUnit), xUnit.net v2 to v3 API migration, MSTest version - upgrades (use migrate-mstest-* skills), TFM upgrades, or UWP/WinUI test - projects. + translation, xUnit.net v3 filter migration (--filter-class, + --filter-trait, --filter-query), Directory.Build.props and global.json + configuration, CI/CD pipeline updates, and MTP extension packages. + DO NOT USE FOR: migrating between test frameworks (MSTest/xUnit/NUnit), + xUnit.net v2 to v3 API migration, MSTest version upgrades (use + migrate-mstest-* skills), TFM upgrades, or UWP/WinUI test projects. license: MIT --- @@ -35,7 +35,7 @@ Migrate a .NET test solution from VSTest to Microsoft.Testing.Platform (MTP). Th ## When Not to Use -- The project already runs on Microsoft.Testing.Platform -- migration is done +- The project already runs on Microsoft.Testing.Platform and there is no remaining MTP behavioral difference to resolve (e.g., exit code 8 for zero tests discovered) - Migrating between test frameworks (e.g., MSTest to xUnit.net) -- different effort entirely - The project builds UWP or packaged WinUI test projects -- MTP does not support these yet - The solution mixes .NET and non-.NET test adapters (e.g., JavaScript or C++ adapters) -- VSTest is required @@ -208,7 +208,44 @@ VSTest-specific arguments must be translated to MTP equivalents. Build-related a **MSTest, NUnit, and xUnit.net v2 (with `YTest.MTP.XUnit2`)**: The VSTest `--filter` syntax is identical on both VSTest and MTP. No changes needed. -**xUnit.net v3 (native MTP)**: xUnit.net v3 does NOT support the VSTest `--filter` syntax on MTP. See the **VSTest → MTP filter translation** section in the `filter-syntax` skill for the complete translation table. Key translation example: +**xUnit.net v3 (native MTP)**: xUnit.net v3 does NOT support the VSTest `--filter` syntax on MTP. You must translate filters to xUnit.net v3's native filter options. + +#### xUnit.net v3 filter flags + +| Flag | Description | +|------|-------------| +| `--filter-class "name"` | Run all tests in a given class. Supports wildcards (`*`). | +| `--filter-not-class "name"` | Exclude all tests in a given class | +| `--filter-method "name"` | Run a specific test method | +| `--filter-not-method "name"` | Exclude a specific test method | +| `--filter-namespace "name"` | Run all tests in a namespace | +| `--filter-not-namespace "name"` | Exclude all tests in a namespace | +| `--filter-trait "name=value"` | Run tests with a matching trait | +| `--filter-not-trait "name=value"` | Exclude tests with a matching trait | + +Multiple values can be specified with a single flag: `--filter-class Foo Bar`. + +#### VSTest → xUnit.net v3 filter translation table + +| VSTest `--filter` syntax | xUnit.net v3 MTP equivalent | Notes | +|---|---|---| +| `FullyQualifiedName~ClassName` | `--filter-class *ClassName*` | Wildcards required for substring match | +| `FullyQualifiedName=Ns.Class.Method` | `--filter-method Ns.Class.Method` | Exact match on fully qualified method | +| `Name=MethodName` | `--filter-method *MethodName*` | Wildcards for substring match | +| `Category=Value` (trait) | `--filter-trait "Category=Value"` | Filter by trait name/value pair | +| Complex expressions | `--filter-query "expr"` | Uses xUnit.net query filter language (see below) | + +#### xUnit.net v3 query filter language + +For complex expressions, use `--filter-query` with a path-segment syntax: + +```text +////[traitName=traitValue] +``` + +Each segment matches against: assembly name, namespace, class name, method name. Use `*` for "match all" in any segment. Documentation: + +#### Translation example ```shell # VSTest diff --git a/catalog/Testing/Official-DotNet-Test/skills/migrate-xunit-to-xunit-v3/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/migrate-xunit-to-xunit-v3/SKILL.md index 6d2beb6..759ce64 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/migrate-xunit-to-xunit-v3/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/migrate-xunit-to-xunit-v3/SKILL.md @@ -5,7 +5,9 @@ description: > USE FOR: upgrading xunit to xunit.v3. DO NOT USE FOR: migrating between test frameworks (MSTest/NUnit to xUnit.net), migrating from VSTest to Microsoft.Testing.Platform - (use migrate-vstest-to-mtp). + (use migrate-vstest-to-mtp). For xUnit v3 MTP filter syntax + (--filter-class, --filter-trait, --filter-query), also load + migrate-vstest-to-mtp. license: MIT --- @@ -34,7 +36,9 @@ Migrate .NET test projects from xUnit.net v2 to xUnit.net v3. The outcome is a s > **Commit strategy:** Commit after each major step so the migration is reviewable and bisectable. Separate project file changes from code changes. -### Step 1: Identify xUnit.net projects +> **Prioritization:** Steps 1-5 are required for every migration. Steps 6-12 are conditional — only apply the ones relevant to the project's code patterns. Skip steps that don't apply. + +### Step 1: Identify xUnit.net projects and verify compatibility Search for test projects referencing xUnit.net v2 packages: @@ -48,20 +52,9 @@ Search for test projects referencing xUnit.net v2 packages: Make sure to check the package references in project files, MSBuild props and targets files, like `Directory.Build.props`, `Directory.Build.targets`, and `Directory.Packages.props`. -### Step 2: Verify compatibility - -1. Verify target framework compatibility: xUnit.net v3 requires **.NET 8+** or **.NET Framework 4.7.2+**. For test library projects, .NET Standard 2.0 is also supported. -2. If any of the test projects have non-compatible target frameworks, STOP here and DON'T do anything. Only tell the user to upgrade the target framework first before migrating xUnit.net. -3. Verify project compatibility: xUnit.net v3 only supports SDK-style projects. If any test projects are non-SDK-style, STOP here and DON'T do anything. Only tell the user to migrate to SDK-style projects first before migrating xUnit.net. - -### Step 3: Establish a baseline - -Run `dotnet test` to establish a baseline of test pass/fail counts. When running `dotnet test`, ensure that: - -- You run `dotnet test` without any additional arguments (i.e., don't pass `--no-restore` or `--no-build`). -- Ensure you redirect the command output to a file and read the output from that file. +Verify target framework compatibility: xUnit.net v3 requires **.NET 8+** or **.NET Framework 4.7.2+**. For test library projects, .NET Standard 2.0 is also supported. If any test projects have non-compatible target frameworks, STOP here — tell the user to upgrade the target framework first. Also verify the project uses SDK-style format. -### Step 4: Update package references +### Step 2: Update package references 1. Update any `PackageReference` or `PackageVersion` items for the new package names, based on the following mapping: @@ -73,7 +66,7 @@ Run `dotnet test` to establish a baseline of test pass/fail counts. When running 2. Update all `xunit.v3.*` packages to the latest correct version available on NuGet. Also update `xunit.runner.visualstudio` to the latest version. -### Step 5: Set `OutputType` to `Exe` +### Step 3: Set `OutputType` to `Exe` In each test project (excluding test library projects), set `OutputType` to `Exe` in the project file: @@ -89,15 +82,25 @@ Depending on the solution in hand, there might be a centralized place where this - If all test projects share a name pattern (e.g., `*.Tests.csproj`), add a conditional property group in `Directory.Build.props` that applies only to those projects, like `Exe`. Adjust the condition as needed to target only test projects. - Otherwise, add the `Exe` property to each test project file individually. -### Step 6: Remove `Xunit.Abstractions` usings +### Step 4: Configure test platform + +Preserve the same test platform that was used with xUnit.net v2. xUnit.net v2 always uses VSTest except if the project used `YTest.MTP.XUnit2`. + +- If the project had a reference to `YTest.MTP.XUnit2`: + - Remove the reference to `YTest.MTP.XUnit2` completely. + - Add `true` to `Directory.Build.props` under an unconditional `PropertyGroup`. +- If the project did NOT reference `YTest.MTP.XUnit2` (the common case): + - Add `false` to `Directory.Build.props` under an unconditional `PropertyGroup`. If `Directory.Build.props` doesn't exist, create it. This keeps the project on VSTest. + +### Step 5: Remove `Xunit.Abstractions` usings Find any `using Xunit.Abstractions;` directives in C# files and remove them completely. -### Step 7: Address `async void` breaking change +### Step 6: Address `async void` breaking change (if applicable) In xUnit.net v3, `async void` test methods are no longer supported and will fail to compile. Search for any test methods declared with `async void` and change them to `async Task`. Test methods can be identified via the `[Fact]` or `[Theory]` attributes or other test attributes. -### Step 8: Address breaking change of attributes +### Step 7: Address breaking change of attributes (if applicable) In xUnit.net v3, some attributes were updated so that they accept a `System.Type` instead of two strings (fully qualified type name and assembly name). These attributes are: @@ -108,7 +111,7 @@ In xUnit.net v3, some attributes were updated so that they accept a `System.Type For example, `[assembly: CollectionBehavior("MyNamespace.MyCollectionFactory", "MyAssembly")]` must be converted to `[assembly: CollectionBehavior(typeof(MyNamespace.MyCollectionFactory))]`. -### Step 9: Inheriting from FactAttribute or TheoryAttribute +### Step 8: Inheriting from FactAttribute or TheoryAttribute (if applicable) Identify if there are any custom attributes that inherit from `FactAttribute` or `TheoryAttribute`. These custom user-defined attributes must now provide source information. For example, if the attribute looked like this: @@ -135,7 +138,7 @@ internal sealed class MyFactAttribute : FactAttribute } ``` -### Step 10: Inheriting from BeforeAfterTestAttribute +### Step 9: Inheriting from BeforeAfterTestAttribute (if applicable) Identify if there are any custom attributes that inherit from `BeforeAfterTestAttribute`. These custom user-defined attributes must update their method signatures. Previously, they would have `Before`/`After` overrides that look like this: @@ -173,25 +176,11 @@ it must be changed to this: } ``` -### Step 11: Address new xUnit analyzer warnings - -xunit.v3 introduced new analyzer warnings. You should attempt to address them. +### Step 10: Address new xUnit analyzer warnings (if applicable) -One of the most notable warnings is [xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken](https://xunit.net/xunit.analyzers/rules/xUnit1051). Identify the calls to such methods, if any, and pass the cancellation token. +xunit.v3 introduced new analyzer warnings. The most notable is xUnit1051 (use `TestContext.Current.CancellationToken` for methods accepting `CancellationToken`). Address these if present. -### Step 12: Test platform selection - -You should keep the same test platform that was used with xunit 2. - -Note that xunit 2 is always VSTest except if the user used YTest.MTP.XUnit2. - -- If user had a reference to YTest.MTP.XUnit2: - - Remove the reference to YTest.MTP.XUnit2 completely. - - Add `true` to Directory.Build.props under an unconditional PropertyGroup. -- If user didn't have a reference to YTest.MTP.XUnit2: - - Add `false` to Directory.Build.props under an unconditional PropertyGroup. - -### Step 13: Migrate `Xunit.SkippableFact` +### Step 11: Migrate `Xunit.SkippableFact` (if applicable) If there are any package references to `Xunit.SkippableFact`, remove all these package references entirely. @@ -202,19 +191,11 @@ Then, follow these steps to eliminate usages of APIs coming from the removed pac - Change `Skip.If` method calls to `Assert.SkipWhen`. - Change `Skip.IfNot` method calls to `Assert.SkipUnless`. -### Step 14: Update `Xunit.Combinatorial` NuGet package - -Find package references of `Xunit.Combinatorial` and update them from 1.x to the latest 2.x version available. - -### Step 15: Update `Xunit.StaFact` NuGet package - -Find package references of `Xunit.StaFact` and update them from 1.x to the latest 3.x version available. - -### Step 16: Build the solution +### Step 12: Update companion packages (if applicable) -Now, build the solution to identify any remaining compilation errors that might not have been addressed by previous instructions. -Fix any straightforward errors that show up, and keep iterating and fixing more. +- `Xunit.Combinatorial` 1.x → latest 2.x +- `Xunit.StaFact` 1.x → latest 3.x -You can also look into and to help with the remaining compilation errors. +### Step 13: Build and verify -You can fix as much as you can, and it's okay if not everything is fixed. Just tell the user that there are remaining errors that need to be manually addressed. +Build the solution and fix any remaining compilation errors. Run `dotnet test` to verify all tests pass with the same results as before migration. diff --git a/catalog/Testing/Official-DotNet-Test/skills/test-anti-patterns/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/test-anti-patterns/SKILL.md index 6cbbbfa..762da91 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/test-anti-patterns/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/test-anti-patterns/SKILL.md @@ -1,6 +1,18 @@ --- name: test-anti-patterns -description: "Quick pragmatic detection-focused review of .NET test code for anti-patterns that undermine reliability and diagnostic value. Use when asked to audit test quality, investigate flaky or coupled tests, find duplication or magic values, or when tests pass but don't actually verify anything. Best for identifying and prioritizing issues in existing tests with severity-ranked findings and targeted remediation guidance. Catches assertion gaps, swallowed exceptions, always-true assertions, flakiness indicators, test coupling, over-mocking, naming issues, magic values, duplicate tests, and structural problems. Do NOT use for direct MSTest API rewrites or implementation-only fixes (for example swapped Assert.AreEqual argument order or converting `DynamicData` from `IEnumerable` to `ValueTuple`) — use writing-mstest-tests instead. For a deep formal audit based on academic test smell taxonomy, use exp-test-smell-detection instead. Works with MSTest, xUnit, NUnit, and TUnit." +description: > + Detection-focused review of .NET test code for anti-patterns that + undermine reliability and diagnostic value. + USE FOR: audit test quality, review test code, find test anti-patterns, + tests pass but don't verify anything, flaky tests, ordering dependency, + duplicate tests, magic values, missing/no assertions, swallowed + exceptions, always-true assertions, over-mocking, test coupling, coverage + touching, coverage inflation. + DO NOT USE FOR: writing new tests (use writing-mstest-tests), direct + MSTest API rewrites or implementation-only fixes such as swapped + Assert.AreEqual argument order, running tests (use run-tests), migrating + between frameworks (use migration skills), deep formal audit based on + academic test smell taxonomy (use test-smell-detection). license: MIT --- @@ -25,7 +37,7 @@ Quick, pragmatic analysis of .NET test code for anti-patterns and quality issues - User wants to run or execute tests (use `run-tests`) - User wants to migrate between test frameworks or versions (use migration skills) - User wants to measure code coverage (out of scope) -- User wants a deep formal test smell audit with academic taxonomy and extended catalog (use `exp-test-smell-detection`) +- User wants a deep formal test smell audit with academic taxonomy and extended catalog (use `test-smell-detection`) ## Inputs @@ -52,6 +64,8 @@ Check each test file against the anti-pattern catalog below. Report findings gro | Anti-Pattern | What to Look For | |---|---| | **No assertions** | Test methods that execute code but never assert anything. A passing test without assertions proves nothing. | +| **Coverage touching** | Test class that methodically calls every public method on a type — often in alphabetical or declaration order — without asserting meaningful outcomes. Each test typically does `var result = sut.MethodName(...)` with no assertion, or only a trivial `Assert.IsNotNull(result)`. The intent is to inflate code-coverage metrics rather than verify behavior. Distinct from a single assertion-free test: the pattern is *systematic* coverage of the surface area with no real verification. | +| **Self-referential assertion** | Asserts that the output of an operation equals its input when the operation is expected to be an identity or no-op, e.g. `Assert.AreEqual(input, Parse(input.ToString()))` or `Assert.AreEqual(x, Identity(x))`. The test is tautological — it can only fail if the round-trip is broken, but it never verifies that a *transformation* actually happened. Also catches `Assert.AreEqual(dto.Name, dto.Name)` (asserting a field against itself). | | **Swallowed exceptions** | `try { ... } catch { }` or `catch (Exception)` without rethrowing or asserting. Failures are silently hidden. | | **Assert in catch block only** | `try { Act(); } catch (Exception ex) { Assert.Fail(ex.Message); }` -- use `Assert.ThrowsException` or equivalent instead. The test passes when no exception is thrown even if the result is wrong. | | **Always-true assertions** | `Assert.IsTrue(true)`, `Assert.AreEqual(x, x)`, or conditions that can never fail. | diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-gap-analysis/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/test-gap-analysis/SKILL.md similarity index 98% rename from catalog/Platform/Official-DotNet-Experimental/skills/exp-test-gap-analysis/SKILL.md rename to catalog/Testing/Official-DotNet-Test/skills/test-gap-analysis/SKILL.md index b5580be..734b1e3 100644 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-gap-analysis/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/test-gap-analysis/SKILL.md @@ -1,6 +1,6 @@ --- -name: exp-test-gap-analysis -description: "Performs pseudo-mutation analysis on .NET production code to find gaps in existing test suites. Use when the user asks to find weak tests, discover untested edge cases, check if tests would catch a bug, or evaluate test effectiveness through mutation-style reasoning. Analyzes production code for mutation points (boundary conditions, boolean flips, null returns, exception removal, arithmetic changes) and checks whether existing tests would detect each mutation. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), detecting test anti-patterns (use test-anti-patterns), measuring assertion diversity (use exp-assertion-quality), or running actual mutation testing tools." +name: test-gap-analysis +description: "Performs pseudo-mutation analysis on .NET production code to find gaps in existing test suites. Use when the user asks to find weak tests, discover untested edge cases, check if tests would catch a bug, or evaluate test effectiveness through mutation-style reasoning. Analyzes production code for mutation points (boundary conditions, boolean flips, null returns, exception removal, arithmetic changes) and checks whether existing tests would detect each mutation. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), detecting test anti-patterns (use test-anti-patterns), measuring assertion diversity (use assertion-quality), or running actual mutation testing tools." license: MIT --- @@ -35,7 +35,7 @@ This skill performs **static pseudo-mutation** — reasoning about mutations wit - User wants to write new tests from scratch (use `writing-mstest-tests`) - User wants to detect test anti-patterns like flakiness or poor naming (use `test-anti-patterns`) -- User wants to measure assertion variety (use `exp-assertion-quality`) +- User wants to measure assertion variety (use `assertion-quality`) - User wants to run an actual mutation testing framework like Stryker (help them directly) - User only wants code coverage numbers (out of scope) diff --git a/catalog/Testing/Official-DotNet-Test/skills/test-gap-analysis/manifest.json b/catalog/Testing/Official-DotNet-Test/skills/test-gap-analysis/manifest.json new file mode 100644 index 0000000..69b5926 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/test-gap-analysis/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Testing", + "compatibility": "Requires a .NET test project or solution." +} diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-smell-detection/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/SKILL.md similarity index 94% rename from external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-smell-detection/SKILL.md rename to catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/SKILL.md index 546cd98..148f85f 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-smell-detection/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/SKILL.md @@ -1,6 +1,6 @@ --- -name: exp-test-smell-detection -description: "Deep formal test smell audit based on academic research taxonomy (testsmells.org). Detects 19 categorized smell types — conditional logic, mystery guests, sensitive equality, eager tests, and more — with calibrated severity and research-backed remediation. Use for comprehensive test suite health assessments. For a quick pragmatic review, use test-anti-patterns instead. DO NOT USE FOR: writing new tests (use writing-mstest-tests), evaluating assertion quality specifically (use exp-assertion-quality), or finding test duplication and boilerplate (use exp-test-maintainability)." +name: test-smell-detection +description: "Deep formal test smell audit based on academic research taxonomy (testsmells.org). Detects 19 categorized smell types — conditional logic, mystery guests, sensitive equality, eager tests, and more — with calibrated severity and research-backed remediation. Use for comprehensive test suite health assessments. For a quick pragmatic review, use test-anti-patterns instead. DO NOT USE FOR: writing new tests (use writing-mstest-tests), evaluating assertion quality specifically (use assertion-quality), or finding test duplication and boilerplate (use exp-test-maintainability)." license: MIT --- @@ -34,7 +34,7 @@ Test smells erode confidence in a test suite and inflate maintenance costs: ## When Not to Use - User wants a quick pragmatic test review (use `test-anti-patterns` — faster, covers the most common issues) -- User wants to evaluate assertion diversity specifically (use `exp-assertion-quality`) +- User wants to evaluate assertion diversity specifically (use `assertion-quality`) - User wants to find duplicated boilerplate across tests (use `exp-test-maintainability`) - User wants to write new tests from scratch (help them directly) - User wants to fix a specific failing test (diagnose and fix directly) @@ -50,7 +50,7 @@ Test smells erode confidence in a test suite and inflate maintenance costs: ### Step 1: Gather the test code -Read all test files the user provides. If the user points to a directory or project, scan for all test files by looking for test framework markers — see the `exp-dotnet-test-frameworks` skill for .NET-specific markers. +Read all test files the user provides. If the user points to a directory or project, scan for all test files by looking for test framework markers — see the `dotnet-test-frameworks` skill for .NET-specific markers. For a thorough audit, also consult the [extended smell catalog](references/test-smell-catalog.md) which covers 9 additional smell types beyond the core 10 below. @@ -79,7 +79,7 @@ Tests that depend on external resources — files on disk, databases, network en Tests that call sleep or delay functions to wait for a condition. These introduce non-deterministic timing and slow down the suite. **Severity:** High -**Detection:** Calls to sleep/delay functions inside test methods. See the `exp-dotnet-test-frameworks` skill for .NET-specific patterns. +**Detection:** Calls to sleep/delay functions inside test methods. See the `dotnet-test-frameworks` skill for .NET-specific patterns. #### Smell 4: Assertion-Free Test (Unknown Test) @@ -132,7 +132,7 @@ The test setup method or constructor initializes fields that are not used by eve Tests marked as skipped or disabled. These add overhead and clutter, and the underlying issue they were disabled for may never be addressed. **Severity:** Low -**Detection:** Skip/ignore annotations or conditional compilation that disables a test. See the `exp-dotnet-test-frameworks` skill for framework-specific skip attributes. +**Detection:** Skip/ignore annotations or conditional compilation that disables a test. See the `dotnet-test-frameworks` skill for framework-specific skip attributes. ### Step 3: Apply calibration rules @@ -192,7 +192,7 @@ Present the analysis in this structure: | Flagging integration tests for using real resources | Check for integration test markers and adjust severity accordingly | | Flagging loop-over-collection-assert as conditional logic | Only flag loops with branching or complex logic, not assertion iterations | | Flagging obvious count assertions after adding N items | Consider the immediate context — self-documenting numbers are fine | -| Missing framework-specific assertion syntax | Consult the `exp-dotnet-test-frameworks` skill for .NET framework assertion and skip APIs | +| Missing framework-specific assertion syntax | Consult the `dotnet-test-frameworks` skill for .NET framework assertion and skip APIs | | Over-flagging try/catch that captures for assertion | Distinguish swallowed exceptions from capture-and-assert patterns | | Treating skip annotations with reasons same as bare skips | Note that reasoned skips are less concerning than unexplained ones | | Flagging `DoesNotThrow`-style tests as assertion-free | These implicitly assert no exception — note but acknowledge the intent | diff --git a/catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/manifest.json b/catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/manifest.json new file mode 100644 index 0000000..69b5926 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Testing", + "compatibility": "Requires a .NET test project or solution." +} diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/references/test-smell-catalog.md b/catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/references/test-smell-catalog.md similarity index 100% rename from catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/references/test-smell-catalog.md rename to catalog/Testing/Official-DotNet-Test/skills/test-smell-detection/references/test-smell-catalog.md diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-tagging/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/test-tagging/SKILL.md similarity index 98% rename from catalog/Platform/Official-DotNet-Experimental/skills/exp-test-tagging/SKILL.md rename to catalog/Testing/Official-DotNet-Test/skills/test-tagging/SKILL.md index 0419a08..b423463 100644 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-tagging/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/test-tagging/SKILL.md @@ -1,5 +1,5 @@ --- -name: exp-test-tagging +name: test-tagging description: "Analyzes test suites and tags each test with a standardized set of traits (e.g., positive, negative, critical-path, boundary, smoke, regression). Use when the user wants to categorize, audit, or label tests with traits. Do not use for writing new tests, running tests, or migrating test frameworks." license: MIT --- @@ -57,7 +57,7 @@ A single test may have **multiple traits** (e.g., both `negative` and `boundary` ### Step 1: Detect the test framework -Examine project files and source code to determine the framework — see the `exp-dotnet-test-frameworks` skill for the complete detection table (package references, test markers, assertion APIs, and skip annotations). +Examine project files and source code to determine the framework — see the `dotnet-test-frameworks` skill for the complete detection table (package references, test markers, assertion APIs, and skip annotations). ### Step 2: Scan existing traits diff --git a/catalog/Testing/Official-DotNet-Test/skills/test-tagging/manifest.json b/catalog/Testing/Official-DotNet-Test/skills/test-tagging/manifest.json new file mode 100644 index 0000000..69b5926 --- /dev/null +++ b/catalog/Testing/Official-DotNet-Test/skills/test-tagging/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Testing", + "compatibility": "Requires a .NET test project or solution." +} diff --git a/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md index b4b32a4..31cc944 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md @@ -1,6 +1,19 @@ --- name: writing-mstest-tests -description: "Best practices for writing new MSTest 3.x/4.x unit tests and implementing concrete fixes in existing MSTest code. Use when the user asks to write, create, implement, repair, or modernize tests (including fix-it prompts such as 'something seems off, fix issues'). Primary fit for direct code changes like correcting swapped Assert.AreEqual argument order, replacing outdated assertion patterns, and converting DynamicData from IEnumerable to ValueTuple-based data sets. Covers modern assertions, data-driven tests, test lifecycle, MSTest.Sdk, sealed classes, Assert.Throws, DynamicData with ValueTuples, TestContext, and conditional execution. Do NOT use for broad test quality audits, flaky-test investigations, or test smell detection reports — use test-anti-patterns instead." +description: > + Write new MSTest unit tests and implement concrete fixes in existing MSTest code using + MSTest 3.x/4.x modern APIs and best practices. + USE FOR: write unit tests for a class, write MSTest tests, create test class, + fix test assertions, MSTest assertion APIs (StartsWith, EndsWith, MatchesRegex, + IsGreaterThan, IsInRange, HasCount, IsNull), something seems off with my tests, + review tests and fix issues, + fix swapped Assert.AreEqual arguments, replace ExpectedException with Assert.Throws, modernize + test patterns, convert DynamicData to ValueTuples, data-driven tests, test lifecycle setup, + sealed test classes, async test patterns, cancellation token testing, + test parallelization, Parallelize, DoNotParallelize, MSTest.Sdk project setup. + DO NOT USE FOR: broad test quality audits or test smell detection (use test-anti-patterns), + running tests (use run-tests), MSTest version migration (use migrate-mstest-v1v2-to-v3 or + migrate-mstest-v3-to-v4). license: MIT --- @@ -13,6 +26,8 @@ Help users write effective, modern unit tests with MSTest 3.x/4.x using current - User wants to write new MSTest unit tests - User wants to improve or modernize existing MSTest tests by implementing concrete fixes - User asks about MSTest assertion APIs, data-driven patterns, or test lifecycle +- User asks to replace `Assert.IsTrue` with more specific assertions (collections, nulls, types, comparisons) +- User asks to replace hard casts with type-checking assertions in tests - User needs help fixing a specific MSTest test bug or failing assertion - User asks to fix swapped `Assert.AreEqual` argument order (expected first, actual second) - User asks to convert `DynamicData` from `IEnumerable` to ValueTuple-based data @@ -34,6 +49,12 @@ Help users write effective, modern unit tests with MSTest 3.x/4.x using current | Existing test code | No | Current tests to fix, update, or modernize | | Test scenario description | No | What behavior the user wants to test | +## Response Guidelines + +- **Specific API or pattern questions** (assertions, data-driven, lifecycle): Jump directly to the relevant workflow step. Do not follow the full workflow. +- **Write new tests from scratch**: Follow the full workflow. +- **Review and fix existing tests**: Fix only the issues present. Do not add unrelated improvements. + ## Workflow ### Step 1: Determine project setup @@ -109,13 +130,29 @@ public sealed class OrderServiceTests ### Step 3: Use modern assertion APIs -Use the correct assertion for each scenario. Prefer `Assert` class methods over `StringAssert` or `CollectionAssert` where both exist. +Pick the most specific assertion for each test scenario. More specific assertions produce better failure messages and make the test's intent clear: -#### Equality and null checks +| What you are testing | Assertion | +|---|---| +| Two values are equal | `Assert.AreEqual(expected, actual)` | +| Same object instance (reference identity) | `Assert.AreSame(expected, actual)` | +| Value is null | `Assert.IsNull(value)` | +| Value is not null | `Assert.IsNotNull(value)` | +| Collection is empty | `Assert.IsEmpty(collection)` | +| Collection is not empty | `Assert.IsNotEmpty(collection)` | +| Collection has exactly N items | `Assert.HasCount(N, collection)` | +| Collection contains an item | `Assert.Contains(item, collection)` | +| Collection does not contain an item | `Assert.DoesNotContain(item, collection)` | +| Object is a specific type | `Assert.IsInstanceOfType(value)` | +| Code throws an exception | `Assert.ThrowsExactly(() => ...)` | + +Prefer `Assert` class methods over `StringAssert` or `CollectionAssert` where both exist. + +#### Equality, null, and reference checks ```csharp Assert.AreEqual(expected, actual); // Value equality -Assert.AreSame(expected, actual); // Reference equality +Assert.AreSame(expected, actual); // Reference equality -- same object instance Assert.IsNull(value); Assert.IsNotNull(value); ``` @@ -151,8 +188,12 @@ Replace generic `Assert.IsTrue` with specialized assertions -- they give better | Instead of | Use | |---|---| | `Assert.IsTrue(list.Count > 0)` | `Assert.IsNotEmpty(list)` | +| `Assert.IsTrue(list.Count == 0)` | `Assert.IsEmpty(list)` | | `Assert.IsTrue(list.Count() == 3)` | `Assert.HasCount(3, list)` | | `Assert.IsTrue(x != null)` | `Assert.IsNotNull(x)` | +| `Assert.IsTrue(x == null)` | `Assert.IsNull(x)` | +| `Assert.AreEqual(a, b)` for same instance | `Assert.AreSame(a, b)` -- reference identity | +| `Assert.IsTrue(!list.Contains(item))` | `Assert.DoesNotContain(item, list)` | | `list.Single(predicate)` + `Assert.IsNotNull` | `Assert.ContainsSingle(list)` | | `Assert.IsTrue(list.Contains(item))` | `Assert.Contains(item, list)` | @@ -323,29 +364,3 @@ public void LocalOnly_InteractiveTest() { } [DoNotParallelize] // Opt out specific classes public sealed class DatabaseIntegrationTests { } ``` - -## Validation - -- [ ] Test classes are `sealed` -- [ ] Test methods follow `MethodName_Scenario_ExpectedBehavior` naming -- [ ] `Assert.ThrowsExactly` used instead of `[ExpectedException]` -- [ ] Specialized assertions used instead of `Assert.IsTrue` (e.g., `Assert.IsNotNull`, `Assert.AreEqual`) -- [ ] DynamicData uses ValueTuple return types instead of `IEnumerable` -- [ ] Sync initialization done in the constructor, not `[TestInitialize]` -- [ ] `TestContext.CancellationToken` passed to async calls in tests with `[Timeout]` -- [ ] Project builds with zero errors and all tests pass - -## Common Pitfalls - -| Pitfall | Solution | -|---------|----------| -| `Assert.AreEqual(actual, expected)` -- swapped arguments | Always put expected first: `Assert.AreEqual(expected, actual)`. Failure messages show "Expected: X, Actual: Y" so wrong order makes messages confusing | -| `[ExpectedException]` -- obsolete, cannot assert message | Use `Assert.Throws` or `Assert.ThrowsExactly` | -| `items.Single()` -- unclear exception on failure | Use `Assert.ContainsSingle(items)` for better failure messages | -| Hard cast `(MyType)result` -- unclear exception | Use `Assert.IsInstanceOfType(result)` | -| `IEnumerable` for DynamicData | Use `IEnumerable<(T1, T2, ...)>` ValueTuples for type safety | -| Sync setup in `[TestInitialize]` | Initialize in the constructor instead -- enables `readonly` fields and satisfies nullability analyzers | -| `CancellationToken.None` in async tests | Use `TestContext.CancellationToken` for cooperative timeout | -| `public TestContext? TestContext { get; set; }` | Drop the `?` -- MSTest suppresses CS8618 for this property | -| `TestContext TestContext { get; set; } = null!` | Remove `= null!` -- unnecessary, MSTest handles assignment | -| Non-sealed test classes | Seal test classes by default for performance | diff --git a/catalog/Tools/Official-DotNet-MSBuild/agents/build-perf/AGENT.md b/catalog/Tools/Official-DotNet-MSBuild/agents/build-perf/AGENT.md index 611e169..ae0ea50 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/agents/build-perf/AGENT.md +++ b/catalog/Tools/Official-DotNet-MSBuild/agents/build-perf/AGENT.md @@ -18,16 +18,29 @@ Before starting any analysis, verify the context is MSBuild-related. If the work ### Step 1: Establish Baseline - Run the build with binlog: `dotnet build /bl:perf-baseline.binlog -m` -- Replay to diagnostic log: `dotnet msbuild perf-baseline.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` -- Record total build duration (from build output) and node count +- Record total build duration from build output -### Step 2: Top-down Analysis -Analyze the replayed diagnostic log: -1. `grep 'Target Performance Summary' -A 50 full.log` → find dominant targets and their cumulative time -2. `grep 'Task Performance Summary' -A 50 full.log` → find dominant tasks -3. `grep 'Project Performance Summary' -A 50 full.log` → find time-heavy projects -4. `grep -i 'Total analyzer execution time\|analyzer.*elapsed' full.log` → check analyzer overhead -5. `grep -i 'node.*assigned\|Building with' full.log | head -30` → assess parallelism +### Step 2: Top-down Analysis — binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) which is bundled with this plugin. Call `tools/list` for the MCP first if you are unsure which tools are available. + +1. Use overview tool → understand build status and duration +2. Use expensive_projects tool → find the slowest projects +3. Use expensive_targets tool → find dominant targets and their cumulative time +4. Use expensive_tasks tool → find dominant tasks +5. Use expensive_analyzers tool → check analyzer overhead +6. Drill into specific projects with project_target_times tool + +**Important:** The `.binlog` file is a binary format — do NOT try to `cat`, `head`, `strings`, or read it directly. Use only the MCP tools to query it. + +### Alternate flow — text-log replay (when MCP is unavailable) + +1. Replay to diagnostic log: `dotnet msbuild perf-baseline.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` +2. `grep 'Target Performance Summary' -A 50 full.log` → find dominant targets and their cumulative time +3. `grep 'Task Performance Summary' -A 50 full.log` → find dominant tasks +4. `grep 'Project Performance Summary' -A 50 full.log` → find time-heavy projects +5. `grep -i 'Total analyzer execution time\|analyzer.*elapsed' full.log` → check analyzer overhead +6. `grep -i 'node.*assigned\|Building with' full.log | head -30` → assess parallelism ### Step 3: Bottleneck Classification Classify findings into categories: @@ -39,7 +52,9 @@ Classify findings into categories: - **Analyzers**: disproportionate analyzer time → specific analyzer is expensive ### Step 4: Deep Dive -For each identified bottleneck: +For each identified bottleneck, use MCP tools (task_details, search, properties, items) to drill into specifics. + +When MCP is unavailable, fall back to text-log grep: - `grep 'Target "TargetName"' full.log` → find specific target execution across projects - `grep -i 'Csc.*elapsed\|Csc.*duration' full.log` → check compilation times - `grep 'specific pattern' full.log` → search for specific issues diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/binlog-failure-analysis/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/binlog-failure-analysis/SKILL.md index 7a517e7..f1419cc 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/binlog-failure-analysis/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/binlog-failure-analysis/SKILL.md @@ -1,18 +1,41 @@ --- name: binlog-failure-analysis -description: "Analyze MSBuild binary logs to diagnose build failures by replaying binlogs to searchable text logs. Only activate in MSBuild/.NET build context. USE FOR: build errors that are unclear from console output, diagnosing cascading failures across multi-project builds, tracing MSBuild target execution order, investigating common errors like CS0246 (type not found), MSB4019 (imported project not found), NU1605 (package downgrade), MSB3277 (version conflicts), and ResolveProjectReferences failures. Requires an existing .binlog file. DO NOT USE FOR: generating binlogs (use binlog-generation), build performance analysis (use build-perf-diagnostics), non-MSBuild build systems. INVOKES: dotnet msbuild binlog replay, grep, cat, head, tail for log analysis." +description: "Analyze MSBuild binary logs to diagnose build failures. Only activate in MSBuild/.NET build context. USE FOR: build errors that are unclear from console output, diagnosing cascading failures across multi-project builds, tracing MSBuild target execution order, and generally any MSBuild build issues. Requires an existing .binlog file. DO NOT USE FOR: generating binlogs (use binlog-generation), non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, errors, search, items, properties); falls back to dotnet msbuild binlog replay + grep/cat when the MCP is unavailable." license: MIT --- # Analyzing MSBuild Failures with Binary Logs -Use MSBuild's built-in **binlog replay** to convert binary logs into searchable text logs, then analyze with standard tools (`grep`, `cat`, `head`, `tail`, `find`). +This skill diagnoses MSBuild build failures from a `.binlog` file. The preferred +path uses the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the +`binlog` MCP namespace) which is bundled with this plugin. If the MCP server is +not available, fall back to the **binlog replay** workflow at the bottom. -## Build Error Investigation (Primary Workflow) +## Primary workflow — binlog MCP -### Step 1: Replay the binlog to text logs +The MCP server exposes structured tools for inspecting a `.binlog` without +parsing text logs. Call them directly instead of replaying the binlog to a text +file. Call `tools/list` for the MCP first if you are unsure which tools are available. -Replay produces multiple focused log files in one pass: +**Important constraints:** +- The `.binlog` file is a **binary format** — do NOT try to `cat`, `head`, `strings`, or read it directly. Use only the MCP tools to query it. +- The **original source/project files might or might NOT be available on disk**. Project files (.csproj, .props, .targets, App.config, etc.) - if you cannot locate them on disk, they can only be read from within the binlog via MCP tools (e.g., embedded/source file retrieval). +- **Synthesize findings as you go.** Do not spend all available time investigating — once you have enough evidence, present your conclusions. A partial answer with clear reasoning is better than timing out mid-investigation. + +Use the available MCP server tools to query the binary log for: +- Build errors and warnings +- MSBuild properties and their values +- MSBuild items (PackageReference, ProjectReference, etc.) +- Project evaluation data +- Target execution details +- File contents embedded in the binlog + +## Fallback workflow — text-log replay (when MCP is unavailable) + +Use this only when the MCP server cannot be started (for example, on an older +SDK or in an offline environment without access to the `dotnet-tools` NuGet feed). + +### Replay the binlog to text logs ```bash dotnet msbuild build.binlog -noconlog \ @@ -21,90 +44,20 @@ dotnet msbuild build.binlog -noconlog \ -fl2 -flp2:warningsonly;logfile=warnings.log ``` -> **PowerShell note:** Use `-flp:"v=diag;logfile=full.log;performancesummary"` (quoted semicolons). +> **PowerShell note:** Use `-flp:"v=diag;logfile=full.log;performancesummary"` +> (quoted semicolons). -### Step 2: Read the errors +### Search the text logs ```bash cat errors.log -``` - -This gives all errors with file paths, line numbers, error codes, and project context. - -### Step 3: Search for context around specific errors - -```bash -# Find all occurrences of a specific error code with surrounding context grep -n -B2 -A2 "CS0246" full.log - -# Find which projects failed to compile grep -i "CoreCompile.*FAILED\|Build FAILED\|error MSB" full.log - -# Find project build order and results -grep "done building project\|Building with" full.log | head -50 -``` - -### Step 4: Detect cascading failures - -Projects that never reached `CoreCompile` failed because a dependency failed, not their own code: - -```bash -# List all projects that ran CoreCompile grep 'Target "CoreCompile"' full.log | grep -oP 'project "[^"]*"' - -# Compare against projects that had errors to identify cascading failures -grep "project.*FAILED" full.log ``` -### Step 5: Examine project files for root causes - -```bash -# Read the .csproj of the failing project -cat path/to/Services/Services.csproj - -# Check PackageReference and ProjectReference entries -grep -n "PackageReference\|ProjectReference" path/to/Services/Services.csproj -``` - -**Write your diagnosis as soon as you have enough information.** Do not over-investigate. - -## Additional Workflows - -### Performance Investigation -```bash -# The PerformanceSummary is at the end of full.log -tail -100 full.log # shows target/task timing summary -grep "Target Performance Summary\|Task Performance Summary" -A 50 full.log -``` - -### Dependency/Evaluation Issues -```bash -# Check evaluation properties -grep -i "OutputPath\|IntermediateOutputPath\|TargetFramework" full.log | head -30 -# Check item groups -grep "PackageReference\|ProjectReference" full.log | head -30 -``` - -## Replay reference - -| Command | Purpose | -|---------|---------| -| `dotnet msbuild X.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` | Full diagnostic log with perf summary | -| `dotnet msbuild X.binlog -noconlog -fl -flp:errorsonly;logfile=errors.log` | Errors only | -| `dotnet msbuild X.binlog -noconlog -fl -flp:warningsonly;logfile=warnings.log` | Warnings only | -| `grep -n "PATTERN" full.log` | Search for patterns in the replayed log | -| `dotnet msbuild -pp:preprocessed.xml Proj.csproj` | Preprocess — inline all imports into one file | - ## Generating a binlog (only if none exists) ```bash dotnet build /bl:build.binlog ``` - -## Common error patterns - -1. **CS0246 / "type not found"** → Missing PackageReference — check the .csproj -2. **MSB4019 / "imported project not found"** → SDK install or global.json issue -3. **NU1605 / "package downgrade"** → Version conflict in package graph -4. **MSB3277 / "version conflicts"** → Binding redirect or version alignment issue -5. **Project failed at ResolveProjectReferences** → Cascading failure from a dependency diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/build-parallelism/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/build-parallelism/SKILL.md index 3930391..b2dbaf6 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/build-parallelism/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/build-parallelism/SKILL.md @@ -1,6 +1,6 @@ --- name: build-parallelism -description: "Guide for optimizing MSBuild build parallelism and multi-project scheduling. Only activate in MSBuild/.NET build context. USE FOR: builds not utilizing all CPU cores, speeding up multi-project solutions, evaluating graph build mode (/graph), build time not improving with -m flag, understanding project dependency topology. Note: /maxcpucount default is 1 (sequential) — always use -m for parallel builds. Covers /maxcpucount, graph build for better scheduling and isolation, BuildInParallel on MSBuild task, reducing unnecessary ProjectReferences, solution filters (.slnf) for building subsets. DO NOT USE FOR: single-project builds, incremental build issues (use incremental-build), compilation slowness within a project (use build-perf-diagnostics), non-MSBuild build systems. INVOKES: dotnet build -m, dotnet build /graph, binlog analysis." +description: "Guide for optimizing MSBuild build parallelism and multi-project scheduling. Only activate in MSBuild/.NET build context. USE FOR: builds not utilizing all CPU cores, speeding up multi-project solutions, evaluating graph build mode (/graph), build time not improving with -m flag, understanding project dependency topology. Note: /maxcpucount default is 1 (sequential) — always use -m for parallel builds. Covers /maxcpucount, graph build for better scheduling and isolation, BuildInParallel on MSBuild task, reducing unnecessary ProjectReferences, solution filters (.slnf) for building subsets. DO NOT USE FOR: single-project builds, incremental build issues (use incremental-build), compilation slowness within a project (use build-perf-diagnostics), non-MSBuild build systems. INVOKES: binlog MCP server tools (expensive_projects, expensive_targets, project_target_times); falls back to dotnet build -m, dotnet build /graph, binlog replay + grep." license: MIT --- @@ -51,6 +51,18 @@ license: MIT ## Analyzing Parallelism with Binlog +### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace): + +1. Use expensive_projects tool → find the slowest projects and compare individual vs total build time +2. Use expensive_targets tool → find bottleneck targets +3. Use project_target_times tool → drill into a specific project's target-level timing +4. Ideal: build time should be much less than sum of project times (parallelism) +5. If build time ≈ sum of project times: too many serial dependencies, or one slow project blocking others + +### Fallback: text-log replay (when MCP is unavailable) + Step-by-step: 1. Replay the binlog: `dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/build-perf-diagnostics/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/build-perf-diagnostics/SKILL.md index 7b0d162..5dfad20 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/build-perf-diagnostics/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/build-perf-diagnostics/SKILL.md @@ -1,11 +1,16 @@ --- name: build-perf-diagnostics -description: "Diagnose MSBuild build performance bottlenecks using binary log analysis. Only activate in MSBuild/.NET build context. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline analysis, Target/Task Performance Summary interpretation, and 7 common bottleneck categories. Use after build-perf-baseline has established measurements. DO NOT USE FOR: establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems. INVOKES: dotnet msbuild binlog replay with performancesummary, grep for analysis." +description: "Diagnose MSBuild build performance bottlenecks using binary log analysis. Only activate in MSBuild/.NET build context. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline analysis, Target/Task Performance Summary interpretation, and 7 common bottleneck categories. Use after build-perf-baseline has established measurements. DO NOT USE FOR: establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, errors, search, items, properties); falls back to dotnet msbuild binlog replay + grep/cat when the MCP is unavailable." license: MIT --- ## Performance Analysis Methodology +1. **Generate a binlog**: `dotnet build /bl:{} -m` +2. Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) which is bundled with this plugin + +### Alternate flow when MCP is unavailable: binlog replay to text logs + 1. **Generate a binlog**: `dotnet build /bl:{} -m` 2. **Replay to diagnostic log with performance summary**: ```bash diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/check-bin-obj-clash/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/check-bin-obj-clash/SKILL.md index b7819f9..864ab78 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/check-bin-obj-clash/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/check-bin-obj-clash/SKILL.md @@ -1,6 +1,6 @@ --- name: check-bin-obj-clash -description: "Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. Only activate in MSBuild/.NET build context. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, missing outputs in multi-project builds, multi-targeting builds where project.assets.json conflicts. Diagnoses when multiple projects or TFMs write to the same bin/obj directories due to shared OutputPath, missing AppendTargetFrameworkToOutputPath, or extra global properties like PublishReadyToRun creating redundant evaluations. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems. INVOKES: dotnet msbuild binlog replay, grep for output path analysis." +description: "Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. Only activate in MSBuild/.NET build context. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, missing outputs in multi-project builds, multi-targeting builds where project.assets.json conflicts. Diagnoses when multiple projects or TFMs write to the same bin/obj directories due to shared OutputPath, missing AppendTargetFrameworkToOutputPath, or extra global properties like PublishReadyToRun creating redundant evaluations. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, projects, evaluations, properties, double_writes); falls back to dotnet msbuild binlog replay + grep when the MCP is unavailable." license: MIT --- @@ -35,13 +35,53 @@ Clashes can occur between: Use the `binlog-generation` skill to generate a binary log with the correct naming convention. -## Step 2: Replay the Binary Log to Text +## Primary workflow — binlog MCP + +The MCP server exposes structured tools for inspecting a `.binlog` without +parsing text logs. Call them directly instead of replaying the binlog to a text +file. Call `tools/list` for the MCP first if you are unsure which tools are available. + +**Important constraints:** +- The `.binlog` file is a **binary format** — do NOT try to `cat`, `head`, `strings`, or read it directly. Use only the MCP tools to query it. +- **Synthesize findings as you go.** Do not spend all available time investigating — once you have enough evidence, present your conclusions. + +### Step 2: Get an overview and list projects + +Use the MCP overview and projects tools to understand the build and list all projects that participated. + +### Step 3: Check evaluations and global properties + +Use the MCP `evaluations` and `evaluation_global_properties` tools to find all evaluations per project. Look for: +- Multiple evaluations for the same project (indicates multi-targeting or multiple build configurations) +- Differing global properties between evaluations (`TargetFramework`, `Configuration`, `RuntimeIdentifier`, `SolutionFileName`, `PublishReadyToRun`, etc.) + +### Step 4: Get output paths for each evaluation + +Use the MCP properties tool to query `OutputPath`, `IntermediateOutputPath`, `BaseOutputPath`, and `BaseIntermediateOutputPath` for each project evaluation. + +### Step 5: Check for double writes + +Use the MCP double_writes tool if available — it directly detects files written by multiple project instances. + +### Step 6: Identify clashes + +Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluations: +1. **Normalize paths** - Convert to absolute paths and normalize separators +2. **Group by path** - Find evaluations that share the same OutputPath or IntermediateOutputPath +3. **Filter out non-build evaluations** - Exclude `BuildProjectReferences=false` instances (P2P queries) +4. **Report clashes** - Any group with more than one evaluation indicates a clash + +## Fallback workflow — text-log replay (when MCP is unavailable) + +Use this only when the MCP server cannot be started. + +### Step 2: Replay the Binary Log to Text ```bash dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log ``` -## Step 3: List All Projects +### Step 3: List All Projects ```bash grep -i 'done building project\|Building project' full.log | grep -oP '"[^"]+\.csproj"' | sort -u @@ -49,7 +89,7 @@ grep -i 'done building project\|Building project' full.log | grep -oP '"[^"]+\.c This lists all project files that participated in the build. -## Step 4: Check for Multiple Evaluations per Project +### Step 4: Check for Multiple Evaluations per Project Multiple evaluations for the same project indicate multi-targeting or multiple build configurations: @@ -59,7 +99,7 @@ grep -c 'Evaluation started' full.log grep 'Evaluation started.*\.csproj' full.log ``` -## Step 5: Check Global Properties for Each Evaluation +### Step 5: Check Global Properties for Each Evaluation For each project, query the build properties to understand the build configuration: @@ -88,7 +128,7 @@ When analyzing clashes, filter evaluations based on the type of clash you're inv 3. **Always exclude `BuildProjectReferences=false`**: These are P2P metadata queries, not actual builds that write files. -## Step 6: Get Output Paths for Each Project +### Step 6: Get Output Paths for Each Project Query each project's output path properties: @@ -103,7 +143,7 @@ dotnet msbuild MyProject.csproj -getProperty:BaseOutputPath dotnet msbuild MyProject.csproj -getProperty:BaseIntermediateOutputPath ``` -## Step 7: Identify Clashes +### Step 7: Identify Clashes Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluations: @@ -111,7 +151,7 @@ Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluati 2. **Group by path** - Find evaluations that share the same OutputPath or IntermediateOutputPath 3. **Report clashes** - Any group with more than one evaluation indicates a clash -## Step 8: Verify Clashes via CopyFilesToOutputDirectory (Optional) +### Step 8: Verify Clashes via CopyFilesToOutputDirectory (Optional) As additional evidence for OutputPath clashes, check if multiple project builds execute the `CopyFilesToOutputDirectory` target to the same path. Note that not all clashes manifest here - compilation outputs and other targets may also conflict. @@ -129,7 +169,7 @@ Look for evidence of clashes in the messages: The `SkipUnchangedFiles` skip message often masks clashes - the build succeeds but is vulnerable to race conditions in parallel builds. -## Step 9: Check CoreCompile Execution Patterns (Optional) +### Step 9: Check CoreCompile Execution Patterns (Optional) To understand which project instance did the actual compilation vs redundant work, check `CoreCompile`: diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/eval-performance/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/eval-performance/SKILL.md index 9d45d2f..308d442 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/eval-performance/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/eval-performance/SKILL.md @@ -1,6 +1,6 @@ --- name: eval-performance -description: "Guide for diagnosing and improving MSBuild project evaluation performance. Only activate in MSBuild/.NET build context. USE FOR: builds slow before any compilation starts, high evaluation time in binlog analysis, expensive glob patterns walking large directories (node_modules, .git, bin/obj), deep import chains (>20 levels), preprocessed output >10K lines indicating heavy evaluation, property functions with file I/O ($([System.IO.File]::ReadAllText(...))), multiple evaluations per project. Covers the 5 MSBuild evaluation phases, glob optimization via DefaultItemExcludes, import chain analysis with /pp preprocessing. DO NOT USE FOR: compilation-time slowness (use build-perf-diagnostics), incremental build issues (use incremental-build), non-MSBuild build systems. INVOKES: dotnet msbuild -pp:full.xml for preprocessing, /clp:PerformanceSummary." +description: "Guide for diagnosing and improving MSBuild project evaluation performance. Only activate in MSBuild/.NET build context. USE FOR: builds slow before any compilation starts, high evaluation time in binlog analysis, expensive glob patterns walking large directories (node_modules, .git, bin/obj), deep import chains (>20 levels), preprocessed output >10K lines indicating heavy evaluation, property functions with file I/O ($([System.IO.File]::ReadAllText(...))), multiple evaluations per project. Covers the 5 MSBuild evaluation phases, glob optimization via DefaultItemExcludes, import chain analysis with /pp preprocessing. DO NOT USE FOR: compilation-time slowness (use build-perf-diagnostics), incremental build issues (use incremental-build), non-MSBuild build systems. INVOKES: binlog MCP server tools (evaluations, evaluation_global_properties, evaluation_properties, imports, properties); falls back to dotnet msbuild -pp:full.xml for preprocessing, /clp:PerformanceSummary." license: MIT --- @@ -18,6 +18,18 @@ Key insight: evaluation happens BEFORE any targets run. Slow evaluation = slow b ## Diagnosing Evaluation Performance +### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) to analyze evaluation performance: + +1. Use the evaluations tool to list all evaluations and their durations +2. Use evaluation_global_properties to check for multiple evaluations with differing global properties +3. Use evaluation_properties to inspect evaluated properties for a specific project+TFM +4. Use imports tool to analyze the import chain depth and structure +5. Use properties tool to check for expensive property function evaluations + +### Fallback: text-log replay and preprocessing (when MCP is unavailable) + ### Using binlog 1. Replay the binlog: `dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log` diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/extension-points/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/extension-points/SKILL.md new file mode 100644 index 0000000..463cc95 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/extension-points/SKILL.md @@ -0,0 +1,177 @@ +--- +name: extension-points +description: "Guide for MSBuild extensibility: CustomBefore/CustomAfter hooks, wildcard imports with alphabetic ordering, import gating with control properties, NuGet package build extension layout (build/buildTransitive), and the MicrosoftCommonPropsHasBeenImported guard. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing MSBuild import and hook patterns, reviewing and fixing extension point anti-patterns in Directory.Build files, fixing missing Exists() guards on imports that break fresh clones, fixing NuGet package hooks being silently dropped instead of appended, making build targets extensible for other projects, injecting custom logic into the build pipeline, creating NuGet packages that extend the build, conditionally disabling imports. DO NOT USE FOR: target authoring patterns (use target-authoring), props vs targets placement (use directory-build-organization), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# MSBuild Extension Points + +How the MSBuild pipeline provides hooks for SDKs, NuGet packages, repos, and users to inject custom logic. + +## CustomBefore / CustomAfter Hooks + +Every major `.targets` file defines import hooks: + +```xml + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets + + + + + + +``` + +### Rules + +- Default path includes version (`v$(MSBuildToolsVersion)`) for side-by-side installations. +- Always check `Exists()`. The file may not be present on every machine. +- **Append** to the property (don't overwrite) to chain multiple hooks: + +```xml + + + $(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets + + +``` + +## Wildcard Import Directories + +MSBuild imports all files in extension directories, sorted alphabetically: + +```xml + +``` + +### Key paths + +| Property | Resolves to | Scope | +|---|---|---| +| `$(MSBuildUserExtensionsPath)` | `%APPDATA%\Microsoft\MSBuild` | Per-user | +| `$(MSBuildExtensionsPath)` | MSBuild install directory | Machine-wide | +| `$(MSBuildProjectExtensionsPath)` | `obj/` directory | Per-project (NuGet) | + +Name files with numeric prefixes for ordering: `01-first.props`, `02-second.props`. + +## Import Gating — Control Properties + +Every wildcard import is gated by a boolean property: + +```xml + + true + true + +``` + +### Available control properties + +| Property | What it disables | +|---|---| +| `ImportDirectoryBuildProps` | Directory.Build.props auto-discovery | +| `ImportDirectoryBuildTargets` | Directory.Build.targets auto-discovery | +| `ImportProjectExtensionProps` | NuGet-generated `*.props` in obj/ | +| `ImportProjectExtensionTargets` | NuGet-generated `*.targets` in obj/ | +| `ImportByWildcardBefore*` | Machine-level ImportBefore extensions | +| `ImportByWildcardAfter*` | Machine-level ImportAfter extensions | + +## NuGet Package Build Extension Layout + +NuGet packages inject build logic via `build/` or `buildTransitive/` folders: + +```text +MyPackage/ + build/ + MyPackage.props ← imported via *.props wildcard + MyPackage.targets ← imported via *.targets wildcard + buildTransitive/ + MyPackage.props ← imported by transitive consumers + MyPackage.targets +``` + +### Rules + +- File names **must match the package ID** exactly. +- `build/` affects direct consumers only. `buildTransitive/` affects the entire dependency chain. +- Props are imported early (before the project), targets are imported late (after the project). + +## Import Guard Pattern + +The `.targets` file ensures `.props` was imported using a guard property: + +```xml + + + true + + + + +``` + +This handles projects that only import `.targets`. + +## Directory.Build Discovery + +MSBuild walks up the directory tree to find the nearest `Directory.Build.props`: + +```xml +<_DirectoryBuildPropsBasePath> + $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props')) + +``` + +Only the **nearest** file is discovered. Nested hierarchies must explicitly import parents: + +```xml + + + <_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) + + +``` + +## Creating Your Own Extension Point + +```xml + + + + + + $(MSBuildProjectDirectory)\MySDK.Before.targets + $(MSBuildProjectDirectory)\MySDK.After.targets + + + + + + BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild + + + + + + + + + + +``` + +## Common Pitfalls + +- **Missing `Exists()` on optional imports** causes build failures when files are absent. +- **Overwriting Custom* properties** drops prior hooks. Append with `;` separator. +- **NuGet package file names not matching package ID** silently skips the import. +- **Nested Directory.Build.props** without parent import loses repo-root settings. diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/extension-points/manifest.json b/catalog/Tools/Official-DotNet-MSBuild/skills/extension-points/manifest.json new file mode 100644 index 0000000..b42d1e3 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/extension-points/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Core", + "compatibility": "Requires a .NET repository with MSBuild project or solution files." +} diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/incremental-build/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/incremental-build/SKILL.md index 9406553..3b1e7e7 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/incremental-build/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/incremental-build/SKILL.md @@ -1,6 +1,6 @@ --- name: incremental-build -description: "Guide for optimizing MSBuild incremental builds. Only activate in MSBuild/.NET build context. USE FOR: builds slower than expected on subsequent runs, 'nothing changed but it rebuilds anyway', diagnosing why targets re-execute unnecessarily, fixing broken no-op builds. Covers 8 common causes: missing Inputs/Outputs on custom targets, volatile properties in output paths (timestamps/GUIDs), file writes outside tracked Outputs, missing FileWrites registration, glob changes, Visual Studio Fast Up-to-Date Check (FUTDC) issues. Key diagnostic: look for 'Building target completely' vs 'Skipping target' in binlog. DO NOT USE FOR: first-time build slowness (use build-perf-baseline), parallelism issues (use build-parallelism), evaluation-phase slowness (use eval-performance), non-MSBuild build systems. INVOKES: dotnet build /bl, binlog replay with diagnostic verbosity." +description: "Guide for optimizing MSBuild incremental builds. Only activate in MSBuild/.NET build context. USE FOR: builds slower than expected on subsequent runs, 'nothing changed but it rebuilds anyway', diagnosing why targets re-execute unnecessarily, fixing broken no-op builds. Covers 8 common causes: missing Inputs/Outputs on custom targets, volatile properties in output paths (timestamps/GUIDs), file writes outside tracked Outputs, missing FileWrites registration, glob changes, Visual Studio Fast Up-to-Date Check (FUTDC) issues. Key diagnostic: look for 'Building target completely' vs 'Skipping target' in binlog. DO NOT USE FOR: first-time build slowness (use build-perf-baseline), parallelism issues (use build-parallelism), evaluation-phase slowness (use eval-performance), non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, search, target details); falls back to dotnet build /bl, binlog replay with diagnostic verbosity." license: MIT --- @@ -58,6 +58,18 @@ Use binary logs (binlogs) to understand exactly why targets ran instead of being ``` The first build establishes the baseline. The second build is the one you want to be incremental. Analyze `second.binlog`. +### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) to analyze the second binlog: + +1. Use the overview tool to check overall build status and duration +2. Use the search tool to find targets that executed vs were skipped — search for "Building target completely", "Building target incrementally", "Skipping target" +3. Use the search tool to find "is newer than output" messages that reveal which input file triggered a rebuild +4. Use target-related tools (target_reasons, project_targets) to inspect why specific targets ran +5. Use the expensive_targets tool to find targets that consumed the most time in the second build — these are your optimization targets + +### Fallback: text-log replay (when MCP is unavailable) + 2. **Replay the second binlog** to a diagnostic text log: ```shell dotnet msbuild second.binlog -noconlog -fl -flp:v=diag;logfile=second-full.log;performancesummary diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/item-management/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/item-management/SKILL.md new file mode 100644 index 0000000..8f44045 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/item-management/SKILL.md @@ -0,0 +1,163 @@ +--- +name: item-management +description: "Patterns for managing MSBuild item groups: Include/Remove/Update semantics, item metadata, batching with %(Metadata), transforms, per-item filtering, and cross-product batching pitfalls. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing item group anti-patterns in .csproj files, reviewing item management for correctness, fixing CS2002 duplicate file warnings from SDK globbing, fixing targets that run more times than expected due to cross-product batching, fixing Include vs Update misuse on SDK-globbed items, fixing FileWrites registration for generated file clean support, moving generated files to IntermediateOutputPath. DO NOT USE FOR: target chain architecture (use target-authoring), property patterns (use property-patterns), incrementality (use incremental-build), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# MSBuild Item Management Patterns + +Canonical patterns for working with item groups, from `Microsoft.Common.CurrentVersion.targets`. + +## Include / Remove / Update — Three Operations + +| Operation | Purpose | When to use | +|---|---|---| +| `Include` | Add new items to the group | Creating items with identity + metadata | +| `Remove` | Remove items matching a pattern | Excluding files or clearing a group | +| `Update` | Modify metadata on existing items | Adding/changing metadata without re-adding | + +### Include — Add Items + +```xml + + + true + + +``` + +### Remove — Subtract Items + +```xml + + + + + + <_CleanOrphanFileWrites Include="@(_CleanPriorFileWrites)" + Exclude="@(_CleanCurrentFileWrites)" /> + + + <_Temporary Remove="@(_Temporary)" /> + +``` + +### Update — Modify Existing Items + +```xml + + + true + Microsoft.CodeAnalysis.Collections.SR + + +``` + +`Update` does not add items — it only modifies items already in the group. + +## Item Batching — %(Metadata) + +When `%(Metadata)` appears in target attributes or task parameters, MSBuild **batches** execution per unique metadata value. + +### Target-level batching (Outputs) + +```xml + + + +``` + +### Task-level batching + +```xml + + +``` + +### Per-item filtering with Condition + +```xml + + <_ResxOutput Include="@(EmbeddedResource->'%(OutputResource)')" + Condition="'%(EmbeddedResource.WithCulture)' == 'false'" /> + +``` + +### Batching rules + +- `%(Metadata)` in `Condition` or `Outputs` → target batches per unique value. +- `%(Metadata)` in task parameters → task batches per unique value. +- **Do not mix `%()` from different item groups** in the same expression — this causes a cross-product (see Common Pitfalls). + +## Item Transforms — @(Item->'expression') + +Transforms create new item lists by applying an expression to each item: + +```xml + + + + + +``` + +## Exclude Pattern — Set Subtraction on Include + +```xml + + + +``` + +`Exclude` only works on `Include` — it cannot be used with `Update` or `Remove`. + +## Conditional Item Inclusion + +```xml + + + + + + + + + +``` + +## PrivateAssets on Tool/Analyzer Packages + +```xml + + + + +``` + +## Common Pitfalls + +### Cross-product batching + +Referencing `%(Metadata)` from two different item groups creates O(N×M) executions: + +```xml + + + + + +``` + +### Generated files in source tree + +Write to `$(IntermediateOutputPath)` (obj/), not the source directory. Source-tree generation pollutes version control and can cause duplicate compilation via globs. + +### Missing FileWrites + +Every file created during a target must be added to `@(FileWrites)` for `dotnet clean` support. diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/item-management/manifest.json b/catalog/Tools/Official-DotNet-MSBuild/skills/item-management/manifest.json new file mode 100644 index 0000000..b42d1e3 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/item-management/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Core", + "compatibility": "Requires a .NET repository with MSBuild project or solution files." +} diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/property-patterns/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/property-patterns/SKILL.md new file mode 100644 index 0000000..eeb0136 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/property-patterns/SKILL.md @@ -0,0 +1,167 @@ +--- +name: property-patterns +description: "MSBuild property definition patterns: conditional defaults, composition/concatenation, path normalization, trailing slash handling, TFM detection helpers, and property evaluation order. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing MSBuild property definition issues in .props or .csproj files, reviewing and fixing shared property configuration anti-patterns, fixing DefineConstants or NoWarn being overwritten instead of appended, fixing unconditional property assignments that prevent project-level overrides, fixing unquoted conditions that fail when properties are empty, fixing hardcoded paths that break cross-platform builds, setting property defaults that can be overridden, understanding property evaluation order and last-write-wins semantics. DO NOT USE FOR: props vs targets placement (use directory-build-organization), item operations (use item-management), target structure (use target-authoring), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# MSBuild Property Patterns + +Canonical property definition and manipulation patterns from the MSBuild repository. + +## Conditional Defaults — The Foundational Pattern + +Set a property **only if not already set**, allowing callers to override: + +```xml + + Debug + AnyCPU + true + +``` + +### Rules + +- Always quote both sides: `'$(Prop)' == ''` +- In `.props`: creates overridable defaults. In `.targets`: creates fallbacks. +- Properties without the condition **cannot be overridden** by earlier imports. + +## Nested Conditional Groups + +Group related properties under a shared condition: + +```xml + + $(DefineConstants);FEATURE_APARTMENT_STATE + $(DefineConstants);FEATURE_APM + true + + + + true + $(DefineConstants);RUNTIME_TYPE_NETCORE + +``` + +Use the outer `Condition` on `PropertyGroup` to avoid repeating the same condition on every property. + +> **Warning:** `$(TargetFramework)` is empty in `.props` files for single-targeting projects until the project body is evaluated. Place `TargetFramework`-conditioned property groups in `.targets` files (or the project file itself), where the value is always available. + +## Composition — Semicolon Concatenation + +Properties that hold lists use semicolons. Always include the existing value when appending: + +```xml + + $(DefineConstants);MY_FEATURE + $(NoWarn);NU5131;IDE0005 + $(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild);netstandard2.0 + +``` + +## Path Normalization and Trailing Slashes + +```xml + + + $(OutDir)\ + + + + + $([MSBuild]::NormalizePath('$(TargetDir)', 'ref', '$(TargetFileName)')) + + + + + + $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)')) + + +``` + +### Preferred path functions + +| Function | Purpose | +|---|---| +| `$([MSBuild]::NormalizePath(...))` | Combine and normalize (cross-platform) | +| `$([System.IO.Path]::Combine(...))` | Combine path segments | +| `$([System.IO.Path]::IsPathRooted(...))` | Check if absolute | +| `HasTrailingSlash(...)` | Check for trailing slash | +| `$([MSBuild]::GetDirectoryNameOfFileAbove(...))` | Walk up directory tree | +| `$(MSBuildThisFileDirectory)` | Directory of current file | + +## Target Framework Detection Helpers + +```xml + + + true + + + + + true + + + + + $(DefineConstants);TEST_ISWINDOWS + +``` + +## Guard Properties + +Mark that a file has been imported to prevent double-imports: + +```xml + + + true + + + + +``` + +## Feature Gating by MSBuild Version + +```xml + + true + +``` + +## Fallback Chains + +Set via primary source first, then fall back: + +```xml + + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToDotNetFrameworkSdkFile('tlbexp.exe')) + $(_NetFxToolsDir)TlbExp.exe + +``` + +## Last Write Wins — Evaluation Order + +MSBuild evaluates properties top-to-bottom. The last assignment wins: + +```xml + +value1 + +value2 + +value3 +``` + +Properties in `.targets` (imported late) override properties in `.props` (imported early) and the project file. + +## Common Pitfalls + +- **Unquoted conditions** (`$(X)==true`) fail when the property is empty. Always quote both sides. +- **Overwriting DefineConstants** (`MY_CONST`) drops all prior constants. Always append with `$(DefineConstants);`. +- **Hardcoded absolute paths** break portability. Use `$(MSBuildThisFileDirectory)` or `$([MSBuild]::NormalizePath(...))`. +- **Missing `Condition` on defaults** makes properties non-overridable. Add `Condition="'$(Prop)' == ''"` for values meant to be defaults. diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/property-patterns/manifest.json b/catalog/Tools/Official-DotNet-MSBuild/skills/property-patterns/manifest.json new file mode 100644 index 0000000..b42d1e3 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/property-patterns/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Core", + "compatibility": "Requires a .NET repository with MSBuild project or solution files." +} diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/resolve-project-references/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/resolve-project-references/SKILL.md index 62b979f..664e088 100644 --- a/catalog/Tools/Official-DotNet-MSBuild/skills/resolve-project-references/SKILL.md +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/resolve-project-references/SKILL.md @@ -38,7 +38,13 @@ The reported time includes **waiting for dependent projects to build** while the ### Step 3: Redirect to task self-time -Guide the user to use the **Task** Performance Summary instead: +Use the **Task** Performance Summary to identify the real bottleneck. + +#### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** expensive_tasks tool to get task self-time rankings directly from the binlog. + +#### Fallback: text-log replay (when MCP is unavailable) ```bash dotnet msbuild build.binlog -noconlog -fl "-flp:v=diag;logfile=full.log;performancesummary" diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/target-authoring/SKILL.md b/catalog/Tools/Official-DotNet-MSBuild/skills/target-authoring/SKILL.md new file mode 100644 index 0000000..4c0ace8 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/target-authoring/SKILL.md @@ -0,0 +1,161 @@ +--- +name: target-authoring +description: "Canonical patterns for writing custom MSBuild targets. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing custom target authoring anti-patterns, reviewing MSBuild target definitions for correctness, diagnosing broken SDK target chains across files (e.g., Directory.Build.targets silently redefining SDK targets), fixing targets that replace CompileDependsOn instead of extending it with $(CompileDependsOn), fixing query targets that return stale results due to Outputs vs Returns misuse, fixing missing Inputs/Outputs causing unnecessary rebuilds, fixing missing FileWrites registration. Covers DependsOnTargets vs BeforeTargets vs AfterTargets, the Build→CoreBuild three-level pattern, hooking into the build pipeline, the $(XxxDependsOn) chain-extension pattern. DO NOT USE FOR: incremental build tuning (use incremental-build), parallelization (use build-parallelism), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# Custom Target Authoring Patterns + +Canonical patterns from `Microsoft.Common.CurrentVersion.targets` in the MSBuild repository. + +## The Three-Level Target Chain + +Every major entry point (Build, Rebuild, Clean) delegates to a **property** listing its dependencies, which chains through Before → Core → After: + +```xml + + + BeforeBuild; + CoreBuild; + AfterBuild + + + + + + + + +``` + +`CoreBuild` delegates to `$(CoreBuildDependsOn)` and includes error handlers: + +```xml + + + + +``` + +### Rules + +- Delegate to a property (`DependsOnTargets="$(MyTargetDependsOn)"`), not hardcoded targets. +- `OnError` goes inside the orchestrating target to ensure cleanup runs even on failure. +- Empty Before/After targets are extensibility points. Users override them; SDKs never put logic in them. + +## Chain Extension — Append, Never Overwrite + +When adding a custom target to an existing chain, **append** to the `DependsOn` property: + +```xml + + + $(CompileDependsOn);MyCodeGenTarget + + + + + MyCodeGenTarget + +``` + +## DependsOnTargets vs BeforeTargets vs AfterTargets + +| Mechanism | Defined in | Best for | +|---|---|---| +| `DependsOnTargets` | The target that needs deps | Target explicitly requires others | +| `BeforeTargets` | The injecting target | Insert before a target you don't own | +| `AfterTargets` | The injecting target | Insert after a target you don't own | + +Validation targets use `BeforeTargets` to intercept all entry points: + +```xml + + +``` + +**Rules:** + +- Use `DependsOnTargets` when your target needs specific prerequisites. +- Use `BeforeTargets`/`AfterTargets` when injecting into a pipeline you don't own. +- Prefer `BeforeTargets="CoreCompile"` over modifying `$(CompileDependsOn)` when you don't control the targets file. + +## Returns vs Outputs + +```xml + + + + + +``` + +- **`Returns`** specifies what the MSBuild task receives when calling this project. Use for inter-project communication. +- **`Outputs`** on inner targets is for incrementality (timestamp checks). Use for up-to-date detection. +- Never mix the two purposes. Query targets (`GetTargetPath`, `GetTargetFrameworks`) should use `Returns`, not `Outputs`. + +## Target Naming Conventions + +| Pattern | Meaning | Example | +|---|---|---| +| `_PrefixedName` | Internal/private target | `_TimeStampBeforeCompile` | +| `CoreXxx` | The actual implementation | `CoreBuild`, `CoreCompile` | +| `BeforeXxx` / `AfterXxx` | Empty extensibility hooks | `BeforeBuild`, `AfterCompile` | +| `PrepareXxx` | Setup/validation phase | `PrepareForBuild` | +| `ResolveXxx` | Discovery/resolution phase | `ResolveReferences` | +| `GetXxx` | Lightweight query (no side effects) | `GetTargetPath` | + +## Complete Custom Target Template + +```xml + + + + _ValidateMyFeatureInputs; + BeforeMyFeature; + CoreMyFeature; + AfterMyFeature + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## Common Pitfalls + +- **Overwriting `DependsOn` properties** drops SDK targets silently. Always include `$(ExistingProperty)` when appending. +- **Using `Outputs` on query targets** causes MSBuild to skip them when "up to date," returning stale data. Use `Returns`. +- **Defining targets in `.props`** means `BeforeTargets` on SDK targets have nothing to hook into yet. Move targets to `.targets`. +- **Forgetting `OnError`** in orchestrating targets means file tracking fails on build errors, breaking subsequent incremental builds. diff --git a/catalog/Tools/Official-DotNet-MSBuild/skills/target-authoring/manifest.json b/catalog/Tools/Official-DotNet-MSBuild/skills/target-authoring/manifest.json new file mode 100644 index 0000000..b42d1e3 --- /dev/null +++ b/catalog/Tools/Official-DotNet-MSBuild/skills/target-authoring/manifest.json @@ -0,0 +1,5 @@ +{ + "version": "0.1.0", + "category": "Core", + "compatibility": "Requires a .NET repository with MSBuild project or solution files." +} diff --git a/cli/ManagedCode.Agents/ManagedCode.Agents.csproj b/cli/ManagedCode.Agents/ManagedCode.Agents.csproj index 435dce3..ad96f59 100644 --- a/cli/ManagedCode.Agents/ManagedCode.Agents.csproj +++ b/cli/ManagedCode.Agents/ManagedCode.Agents.csproj @@ -50,7 +50,7 @@ - + diff --git a/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj b/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj index b143852..cb61459 100644 --- a/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj +++ b/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj @@ -50,7 +50,7 @@ - + diff --git a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs new file mode 100644 index 0000000..c91d48b --- /dev/null +++ b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs @@ -0,0 +1,2178 @@ +// ----------------------------------------------------------------------------- +// SharpConsoleUI command center — the retained-mode, windowed interactive shell. +// +// This is the default surface for the bare `dotnet skills` invocation. (The +// `agents` / `dotnet-agents` wrappers still dispatch their bare invocation to +// the agents-list path in Program.cs; rerouting them through this command +// center is intentionally a follow-up.) It replaces the prompt-first Spectre +// loop in InteractiveConsoleApp.cs with a NavigationView-driven shell: +// * each former Show* screen is a NavigationView page rendered with native +// SharpConsoleUI controls (PanelControl + HorizontalGrid + MarkupControl) +// * SelectionPrompt/Confirm flows become ListControl activation + modal +// windows with ButtonControls +// * mutating actions call the Runtime installers directly and re-render the +// affected page in place +// +// The classic prompt loop survives as RunClassicShellAsync and is used as a +// fallback when stdin/stdout is redirected (CI, pipes, dumb terminals). +// ----------------------------------------------------------------------------- + +using ManagedCode.DotnetSkills.Runtime; +using SharpConsoleUI; +using SharpConsoleUI.Builders; +using SharpConsoleUI.Controls; +using SharpConsoleUI.Core; +using SharpConsoleUI.Drivers; +using SharpConsoleUI.Helpers; +using SharpConsoleUI.Layout; +using SharpConsoleUI.Rendering; +using SharpConsoleUI.Themes; + +namespace ManagedCode.DotnetSkills; + +internal sealed partial class InteractiveConsoleApp +{ + // One selection treatment for every list mode (keyboard-highlight, mouse-hover, click). + // The list control otherwise renders three subtly different states: HighlightBackgroundColor + // for the focused selection, the theme's ListHoverBackgroundColor for mouse hover, and the + // theme's ListUnfocusedHighlightBackgroundColor when the list does not hold focus. We pin all + // of them so the bar looks the same regardless of how the row was reached. + private static readonly Color SelectionBg = new(150, 205, 255); + private static readonly Color SelectionFg = Color.Black; + private static readonly Color UnfocusedSelectionBg = new(44, 62, 92); + private static readonly Color UnfocusedSelectionFg = new(205, 218, 236); + private static readonly Color ShortcutAccent = new(130, 205, 255); + + // Accent palette — RGB equivalents of the xterm-256 color names this UI was originally + // designed around. Kept here so the palette has one canonical home. + private static readonly Color AccentDeepSkyBlue = new(0, 175, 255); // Spectre "deepskyblue1" + private static readonly Color AccentTurquoise = new(0, 215, 215); // Spectre "turquoise2" + private static readonly Color AccentMediumPurple = new(135, 95, 215); // Spectre "mediumpurple2" + private static readonly Color AccentSpringGreen = new(0, 175, 95); // Spectre "springgreen3" + private static readonly Color AccentGreen = new(0, 175, 0); // Spectre "green" + private static readonly Color AccentYellow = new(215, 175, 0); // Spectre "yellow" + private static readonly Color AccentGrey = new(135, 135, 135); // Spectre "grey" + private static readonly Color PanelBorderColor = new(70, 88, 116); // matches the root window border + + // Live shell state for the dynamic status bar. + private ConsoleWindowSystem? _ws; + private ScrollablePanelControl? _activePanel; + private HomeAction? _currentPage; + private StatusBarControl? _statusBar; + private StatusBarItem? _clockItem; + private StatusBarItem? _statusMessage; + // Top status bar shows session identity (project, scope, agent, version) and updates + // live when InteractiveSessionState fires its change events. + private StatusBarControl? _topStatusBar; + private StatusBarItem? _topProjectItem; + private StatusBarItem? _topScopeItem; + private StatusBarItem? _topVersionItem; + // Unsubscribe handle for session-event subscriptions tied to the current page. Each page + // build resets this so subscriptions don't leak across page switches. + private Action? _detachSessionEvents; + + // List-page filter active across rebuilds; cleared on page switch. Bound to the `/` overlay. + private string _searchFilter = string.Empty; + + // Currently selected collection in the master-detail Collections page (Commit 4). + private CollectionCatalogView? _selectedCollection; + + // First-click arms the inline two-stage install button on Collections detail (Commit 4); + // second click commits. Cleared every time the selected collection changes. + private bool _collectionInstallArmed; + + private static readonly Color[] SectionPalette = + { + new(120, 180, 255), + new(120, 220, 160), + new(220, 170, 110), + new(195, 150, 230), + new(235, 150, 150), + new(150, 210, 220), + }; + + /// + /// Entry point for the bare interactive invocation. Launches the SharpConsoleUI + /// command center; falls back to the classic prompt loop when there is no real terminal. + /// + public async Task RunAsync() + { + if (Console.IsInputRedirected || Console.IsOutputRedirected) + { + return await RunClassicShellAsync(); + } + + try + { + toolUpdateStatus = await getToolUpdateStatusAsync(cachePath); + await LoadCatalogsAsync(refreshCatalog: false); + } + catch (Exception exception) + { + Console.Error.WriteLine($"Failed to load the skill catalog: {exception.Message}"); + return 1; + } + + try + { + var windowSystem = new ConsoleWindowSystem(new NetConsoleDriver(RenderMode.Buffer), BuildTheme()); + // Top/bottom system panels are both replaced by interactive StatusBarControl instances — + // the top one carries live session identity (project, scope, version), the bottom one + // carries shortcuts + toast slot. + windowSystem.PanelStateService.ShowTopPanel = false; + windowSystem.PanelStateService.ShowBottomPanel = false; + + CreateCommandCenter(windowSystem); + windowSystem.Run(); + return 0; + } + catch (Exception exception) + { + Console.Clear(); + ExceptionFormatter.WriteException(exception); + return 1; + } + } + + private void CreateCommandCenter(ConsoleWindowSystem ws) + { + _ws = ws; + + var installedCount = SafeCount(GetInstalledSkillCount); + var outdatedCount = SafeCount(GetOutdatedSkillCount); + var actions = GetHomeActions(installedCount, outdatedCount) + .Where(action => action.Action != HomeAction.Exit) + .ToArray(); + + var nav = Controls.NavigationView() + .WithNavWidth(30) + .WithPaneHeader("[bold rgb(120,180,255)] ◆ dotnet skills[/]") + .WithPaneDisplayMode(NavigationViewDisplayMode.Auto) + .WithExpandedThreshold(96) + .WithCompactThreshold(54) + .WithContentBorder(BorderStyle.Rounded) + .WithContentBorderColor(new Color(70, 100, 150)) + .WithContentPadding(1, 0, 1, 0) + .WithContentHeader(true) + .WithSelectedColors(Color.White, new Color(40, 80, 160)) + .AddItem(new NavigationItem("Home", icon: "◈", subtitle: "Session & telemetry"), panel => BuildHomePage(ws, panel)); + + var sectionIndex = 0; + foreach (var section in actions.GroupBy(action => action.Section)) + { + var color = SectionPalette[sectionIndex++ % SectionPalette.Length]; + nav = nav.AddHeader(section.Key, color, header => + { + foreach (var action in section) + { + var captured = action; + header.AddItem( + new NavigationItem(captured.Label, icon: "›", subtitle: captured.Summary) { Tag = captured.Action }, + panel => BuildActionPage(ws, panel, captured.Action)); + } + }); + } + + var navView = nav + .OnSelectedItemChanged((_, e) => RebuildStatusBar(e.NewItem?.Tag as HomeAction?)) + .WithAlignment(HorizontalAlignment.Stretch) + .WithVerticalAlignment(VerticalAlignment.Fill) + .Build(); + + _topStatusBar = new StatusBarControl(stickyBottom: false) + { + StickyPosition = StickyPosition.Top, + HorizontalAlignment = HorizontalAlignment.Stretch, + BackgroundColor = Color.Transparent, + SeparatorChar = "·", + ShortcutLabelSeparator = " ", + }; + + _statusBar = new StatusBarControl(stickyBottom: true) + { + HorizontalAlignment = HorizontalAlignment.Stretch, + BackgroundColor = Color.Transparent, + ShortcutForegroundColor = ShortcutAccent, + SeparatorChar = "·", + ShortcutLabelSeparator = " ", + }; + + // Rule separators above and below the content area (cxpost/cxfiles framing pattern). + var topRule = Controls.Rule(); + var bottomRule = Controls.Rule(); + topRule.StickyPosition = StickyPosition.Top; + bottomRule.StickyPosition = StickyPosition.Bottom; + + // Background gradient (cxpost / cxfiles house style — cool dark blue top to near-black bottom). + var backgroundGradient = ColorGradient.FromColors(new Color(25, 32, 52), new Color(7, 7, 13)); + + new WindowBuilder(ws) + .WithTitle("dotnet skills — command center") + .HideTitle() + .Maximized() + .Movable(false) + .Resizable(false) + .HideTitleButtons() + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(new Color(70, 88, 116)) + .WithBackgroundGradient(backgroundGradient, GradientDirection.Vertical) + .WithAsyncWindowThread(ClockLoopAsync) + .OnKeyPressed((_, e) => HandleGlobalKey(e)) + .OnClosed((_, _) => ws.Shutdown(0)) + .AddControl(_topStatusBar) + .AddControl(topRule) + .AddControl(navView) + .AddControl(bottomRule) + .AddControl(_statusBar) + .BuildAndShow(); + + RebuildStatusBar(null); + RebuildTopStatusBar(); + } + + /// + /// Repopulates the top status bar with current session identity. Called on initial build + /// and from session-change event subscriptions in BuildActionPage/BuildHomePage. + /// + private void RebuildTopStatusBar() + { + var bar = _topStatusBar; + if (bar is null) return; + + bar.BatchUpdate(() => + { + bar.ClearAll(); + _topProjectItem = bar.AddLeftText($"[bold rgb(120,180,255)]◆[/] [bold]dotnet skills[/] [grey50]v{Escape(ToolVersionInfo.CurrentVersion)}[/]"); + bar.AddLeftSeparator(); + bar.AddLeftText($"[grey50]project[/] {Escape(CompactPath(Session.ProjectDirectory ?? Environment.CurrentDirectory))}"); + bar.AddLeftSeparator(); + _topScopeItem = bar.AddLeftText($"[grey50]scope[/] {Escape(Session.Scope.ToString())} [grey50]·[/] [grey50]platform[/] {Escape(Session.Agent.ToString())}"); + + _topVersionItem = bar.AddRightText($"[grey50]catalog[/] {Escape(skillCatalog.CatalogVersion)} [grey50]·[/] {skillCatalog.Skills.Count} skills"); + }); + } + + /// + /// Navigates to a HomeAction page without going through the NavigationView rail — used by + /// the clickable Home metric cards and the command palette. The rail's visual selection + /// state won't follow, but the content panel rebuilds and status bars update. + /// + private void NavigateTo(HomeAction action) + { + if (_ws is null || _activePanel is null) return; + BuildActionPage(_ws, _activePanel, action); + RebuildStatusBar(action); + } + + /// + /// Replaces any prior session-event subscriptions with a fresh one bound to the active page, + /// so flipping Session.Scope/Agent/Project from anywhere refreshes the open page in place. + /// Must be called at the top of every page builder. + /// + private void AttachSessionEvents() + { + _detachSessionEvents?.Invoke(); + Action handler = () => + { + RebuildTopStatusBar(); + RebuildActivePage(); + }; + Session.AgentChanged += handler; + Session.ScopeChanged += handler; + Session.ProjectChanged += handler; + Session.SnapshotChanged += handler; + _detachSessionEvents = () => + { + Session.AgentChanged -= handler; + Session.ScopeChanged -= handler; + Session.ProjectChanged -= handler; + Session.SnapshotChanged -= handler; + }; + } + + private void HandleGlobalKey(KeyPressedEventArgs e) + { + var key = e.KeyInfo; + + // Esc clears an active search filter first, then ends the session. + if (key.Key == ConsoleKey.Escape) + { + if (!string.IsNullOrEmpty(_searchFilter)) + { + _searchFilter = string.Empty; + RebuildActivePage(); + e.Handled = true; + return; + } + _ws?.Shutdown(0); + e.Handled = true; + return; + } + + // Plain `/` opens the search overlay on any list-bearing page (no modifier required). + if ((key.Modifiers & ConsoleModifiers.Control) == 0) + { + if (key.KeyChar == '/' && IsListBearingPage(_currentPage)) + { + ShowSearchOverlay(); + e.Handled = true; + } + return; + } + + switch (key.Key) + { + case ConsoleKey.R: + RefreshCatalogFromUi(); + e.Handled = true; + break; + case ConsoleKey.U when _currentPage == HomeAction.ManageInstalled: + UpdateAllOutdatedFromUi(); + e.Handled = true; + break; + case ConsoleKey.I when _currentPage == HomeAction.SyncProject: + InstallAllRecommendedFromUi(); + e.Handled = true; + break; + case ConsoleKey.Delete when _currentPage == HomeAction.ManageInstalled: + RemoveAllFromUi(); + e.Handled = true; + break; + case ConsoleKey.P: + if (_ws is not null) ShowCommandPalette(_ws); + e.Handled = true; + break; + } + } + + private static bool IsListBearingPage(HomeAction? page) => page is + HomeAction.BrowseSkills or + HomeAction.ManageInstalled or + HomeAction.BrowseCollections or + HomeAction.BrowseBundles or + HomeAction.BrowsePackages or + HomeAction.BrowseAgents; + + // ------------------------------------------------------------------------- + // Page dispatch + // ------------------------------------------------------------------------- + + private void BuildActionPage(ConsoleWindowSystem ws, ScrollablePanelControl panel, HomeAction action) + { + // Page-switch clears transient filters (search + Collections detail selection) so each + // page lands in a clean state. Use NavigateTo if you need to preserve filter context. + if (_currentPage != action) + { + _searchFilter = string.Empty; + _selectedCollection = null; + _collectionInstallArmed = false; + } + _activePanel = panel; + _currentPage = action; + AttachSessionEvents(); + ClearStickyStatus(); + switch (action) + { + case HomeAction.BrowseSkills: BuildSkillBrowserPage(ws, panel); break; + case HomeAction.ManageInstalled: BuildInstalledPage(ws, panel); break; + case HomeAction.BrowseCollections: BuildCollectionsPage(ws, panel); break; + case HomeAction.BrowseBundles: BuildBundlesPage(ws, panel, primaryOnly: true); break; + case HomeAction.BrowsePackages: BuildPackagesPage(ws, panel); break; + case HomeAction.BrowseAgents: BuildAgentsPage(ws, panel); break; + case HomeAction.SyncProject: BuildProjectPage(ws, panel); break; + case HomeAction.Analysis: BuildAnalysisPage(ws, panel); break; + case HomeAction.RemoveAll: BuildRemoveAllPage(ws, panel); break; + case HomeAction.UpdateAll: BuildUpdateAllPage(ws, panel); break; + case HomeAction.Workspace: BuildSettingsPage(ws, panel); break; + case HomeAction.About: BuildAboutPage(panel); break; + default: + panel.ClearContents(); + panel.AddControl(BuildNotePanel(action.ToString(), "[grey50]Not available in this surface.[/]", AccentGrey)); + break; + } + } + + // ------------------------------------------------------------------------- + // Home + // ------------------------------------------------------------------------- + + private void BuildHomePage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + if (_currentPage != null) + { + _searchFilter = string.Empty; + _selectedCollection = null; + _collectionInstallArmed = false; + } + _activePanel = panel; + _currentPage = null; + AttachSessionEvents(); + ClearStickyStatus(); + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var installed = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()); + var outdated = installed.Count(record => !record.IsCurrent); + + panel.AddControl(BuildPropertyPanel("session", AccentDeepSkyBlue, + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("platform", Escape(Session.Agent.ToString())), + ("scope", Escape(Session.Scope.ToString())), + ("project", Escape(CompactPath(Session.ProjectDirectory ?? Environment.CurrentDirectory))), + ("target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"))); + + // catalog telemetry — five native metric cards laid out by HorizontalGrid (responsive flex). + var installedAccent = installed.Count > 0 ? AccentGreen : AccentGrey; + var outdatedAccent = outdated == 0 ? AccentGreen : AccentYellow; + // Cards are clickable navigation targets — click "outdated" to jump to Installed, etc. + var telemetryGrid = Controls.HorizontalGrid() + .Column(col => col.Flex(1).Add(BuildMetricCard("skills", skillCatalog.Skills.Count.ToString(), "in catalog", AccentDeepSkyBlue, () => NavigateTo(HomeAction.BrowseSkills)))) + .Column(col => col.Flex(1).Add(BuildMetricCard("bundles", GetPrimaryBundles().Count.ToString(), "focused", AccentTurquoise, () => NavigateTo(HomeAction.BrowseBundles)))) + .Column(col => col.Flex(1).Add(BuildMetricCard("installed", $"{installed.Count}/{skillCatalog.Skills.Count}", "in current target", installedAccent, () => NavigateTo(HomeAction.ManageInstalled)))) + .Column(col => col.Flex(1).Add(BuildMetricCard("outdated", outdated.ToString(), outdated == 0 ? "all current" : "need update", outdatedAccent, () => NavigateTo(HomeAction.ManageInstalled)))) + .Column(col => col.Flex(1).Add(BuildMetricCard("agents", agentCatalog.Agents.Count.ToString(), "orchestration", AccentMediumPurple, () => NavigateTo(HomeAction.BrowseAgents)))) + .Build(); + panel.AddControl(telemetryGrid); + + if (toolUpdateStatus?.HasUpdate == true) + { + var freshness = toolUpdateStatus.CheckedAt is null + ? "[grey50]latest release detected[/]" + : toolUpdateStatus.UsedCachedValue + ? $"[grey50]cached[/] [grey]{Escape(toolUpdateStatus.CheckedAt.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm"))}[/]" + : $"[grey50]checked[/] [grey]{Escape(toolUpdateStatus.CheckedAt.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm"))}[/]"; + panel.AddControl(BuildBulletPanel("tool update", AccentYellow, + "[bold yellow]New dotnet-skills version available[/]", + $"[grey50]current[/] [grey]{Escape(toolUpdateStatus.CurrentVersion)}[/] [grey50]-> latest[/] [green]{Escape(toolUpdateStatus.LatestVersion ?? "?")}[/]", + $"[green]{Escape(GlobalToolUpdateCommand)}[/]", + $"[grey50]local tool manifest[/] [green]{Escape(LocalToolUpdateCommand)}[/]", + freshness)); + } + + panel.AddControl(BuildBulletPanel("quick start", AccentDeepSkyBlue, + "[grey50]Use the rail on the left to browse and install.[/]", + "[grey]Skills[/] [grey50]browse and install individual catalog skills[/]", + "[grey]Installed[/] [grey50]update or remove what is already installed[/]", + "[grey]Project[/] [grey50]scan the current solution and install recommended skills[/]", + "[grey]Agents[/] [grey50]install orchestration agents into native agent directories[/]")); + } + + // ------------------------------------------------------------------------- + // Native control helpers — every page and modal renders through these. + // ------------------------------------------------------------------------- + + /// + /// A native PanelControl with rounded border, themed header, and accent border color — + /// the visual equivalent of BuildRichShellPanel but drawn directly into the cell buffer + /// so its border aligns with the surrounding window chrome. + /// + private static PanelControl BuildSectionPanel(string title, string body, Color accent) => Controls.Panel() + .WithHeader($"[bold]{Escape(title)}[/]") + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(accent) + .WithPadding(1, 0, 1, 0) + .WithContent(body) + .WithAlignment(HorizontalAlignment.Stretch) + .Build(); + + /// + /// A native metric card: three stacked lines (title accent, value bold, detail grey) inside + /// a rounded PanelControl with an accent border. Used in HorizontalGrid columns. + /// + /// Optional click handler — when non-null, the card becomes a + /// navigation target via its MouseClick event. + private static PanelControl BuildMetricCard(string title, string value, string detail, Color accent, Action? onClick = null) + { + // Multi-line markup body — PanelControl splits on \n and wraps each line. + var body = string.Join("\n", + $"[bold]{Escape(value)}[/]", + $"[grey50]{Escape(detail)}[/]"); + var card = Controls.Panel() + .WithHeader($"[bold]{Escape(title)}[/]") + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(accent) + .WithPadding(1, 0, 1, 0) + .WithContent(body) + .WithAlignment(HorizontalAlignment.Stretch) + .Build(); + if (onClick is not null) + { + card.MouseClick += (_, _) => onClick(); + } + return card; + } + + /// + /// Formats one row of a property grid as " label value" — fixed-width left column so values + /// line up when stacked. Equivalent to BuildRichPropertyGrid's two-column grid but rendered + /// inline as markup text (cheaper, and PanelControl wraps the value if it overflows). + /// + private static string FormatRow(string label, string value) + { + const int labelWidth = 12; + var padded = label.Length >= labelWidth ? label : label + new string(' ', labelWidth - label.Length); + return $"[grey50]{Escape(padded)}[/] {value}"; + } + + /// + /// A native section panel whose body is a property grid built from label/value rows. + /// The native equivalent of BuildRichShellPanel(BuildRichPropertyGrid(...)). + /// + private static PanelControl BuildPropertyPanel(string title, Color accent, params (string Label, string Value)[] rows) + { + var body = string.Join("\n", rows.Select(r => FormatRow(r.Label, r.Value))); + return BuildSectionPanel(title, body, accent); + } + + /// + /// A native section panel containing a single markup line — used for empty-state notes and + /// short status messages. + /// + private static PanelControl BuildNotePanel(string title, string markup, Color accent) + => BuildSectionPanel(title, markup, accent); + + /// + /// A native section panel whose body is a vertical stack of markup lines — used for + /// "quick start", "surface map", and similar bullet-list cards. Lines are joined with \n + /// so PanelControl wraps each independently. + /// + private static PanelControl BuildBulletPanel(string title, Color accent, params string[] lines) + { + var body = string.Join("\n", lines.Where(l => !string.IsNullOrWhiteSpace(l))); + return BuildSectionPanel(title, body, accent); + } + + /// + /// Lays out a sequence of cards in a responsive HorizontalGrid with 1, 2, or 3 columns based + /// on the current console width — the native equivalent of BuildRichCardGrid(maxColumns). + /// Empty columns at the end of the last row are padded with blank MarkupControls so the cards + /// keep equal width. + /// + private static IWindowControl BuildCardGrid(IReadOnlyList cards, int maxColumns = 3) + { + if (cards.Count == 0) + { + return new MarkupControl(new List { "[grey50]No items available.[/]" }); + } + + var consoleWidth = SafeConsole(() => Console.WindowWidth, 120); + var columnCount = consoleWidth >= 190 ? Math.Min(maxColumns, 3) + : consoleWidth >= 130 ? Math.Min(maxColumns, 2) + : 1; + columnCount = Math.Max(1, Math.Min(columnCount, cards.Count)); + + var grid = Controls.HorizontalGrid(); + for (var i = 0; i < columnCount; i++) + { + var columnIndex = i; + grid = grid.Column(col => + { + col.Flex(1); + for (var cardIndex = columnIndex; cardIndex < cards.Count; cardIndex += columnCount) + { + col.Add(cards[cardIndex]); + } + }); + } + + return grid.Build(); + } + + // ------------------------------------------------------------------------- + // Skill browser + // ------------------------------------------------------------------------- + + private void BuildSkillBrowserPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var installed = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()); + var available = skillCatalog.Skills + .Where(skill => installed.All(record => !string.Equals(record.Skill.Name, skill.Name, StringComparison.OrdinalIgnoreCase))) + .OrderBy(skill => CatalogOrganization.GetStackRank(skill.Stack)) + .ThenBy(skill => skill.Stack, StringComparer.Ordinal) + .ThenBy(skill => skill.Name, StringComparer.Ordinal) + .ToArray(); + + var filtered = available.Where(s => MatchesFilter(s.Name, s.Stack, s.Lane)).ToArray(); + + panel.AddControl(BuildPropertyPanel("skill browser", AccentTurquoise, + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("available", $"{filtered.Length}/{available.Length}"), + ("installed", $"{installed.Count}/{skillCatalog.Skills.Count}"))); + AddSearchChip(panel); + + if (available.Length == 0) + { + panel.AddControl(BuildNotePanel("available", "[grey50]Every catalog skill is already installed in this target.[/]", AccentDeepSkyBlue)); + return; + } + if (filtered.Length == 0) + { + panel.AddControl(BuildNotePanel("available", $"[grey50]No skills match “{Escape(_searchFilter)}”.[/]", AccentYellow)); + return; + } + + var list = StyledList("Available skills (Enter for details)") + .MaxVisibleItems(16) + .WithScrollbarVisibility(ScrollbarVisibility.Auto); + foreach (var skill in filtered) + { + // ListControl parses item text as markup; BuildSkillChoiceLabel produces plain + // text containing bracketed stack/lane like "[.NET Foundations / ...]". Escape so + // brackets are not interpreted as Spectre markup tags. + list.AddItem(Escape(BuildSkillChoiceLabel(skill, installed)), skill); + } + list.OnItemActivated((_, item) => + { + if (item.Tag is SkillEntry skill) + { + ShowSkillDetailModal(ws, panel, skill); + } + }); + panel.AddControl(list.Build()); + } + + private void ShowSkillDetailModal(ConsoleWindowSystem ws, ScrollablePanelControl owner, SkillEntry skill) + { + var detail = new IWindowControl[] + { + BuildPropertyPanel(ToAlias(skill.Name), AccentTurquoise, + ("skill", Escape(skill.Name)), + ("collection", Escape(skill.Stack)), + ("lane", Escape(skill.Lane)), + ("version", Escape(skill.Version)), + ("tokens", FormatTokenCount(skill.TokenCount))), + BuildNotePanel("summary", Escape(skill.Description), AccentDeepSkyBlue), + BuildNotePanel("preview", Escape(LoadSkillPreview(skill)), AccentGrey), + }; + + ShowModalNative(ws, $"Skill · {ToAlias(skill.Name)}", detail, + ("Install into current target", () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Install(new[] { skill }, ResolveSkillLayout(), force: false), default(SkillInstallSummary)); + if (summary is null) + Toast($"Install failed for {ToAlias(skill.Name)}", NotificationSeverity.Danger); + else + Toast($"{ToAlias(skill.Name)}: {summary.InstalledCount} written, {summary.SkippedExisting.Count} skipped", NotificationSeverity.Success); + BuildSkillBrowserPage(ws, owner); + }), + ("Force reinstall", () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Install(new[] { skill }, ResolveSkillLayout(), force: true), default(SkillInstallSummary)); + if (summary is null) + Toast($"Install failed for {ToAlias(skill.Name)}", NotificationSeverity.Danger); + else + Toast($"{ToAlias(skill.Name)}: reinstalled ({summary.InstalledCount} written)", NotificationSeverity.Success); + BuildSkillBrowserPage(ws, owner); + })); + } + + // ------------------------------------------------------------------------- + // Installed skills + // ------------------------------------------------------------------------- + + private void BuildInstalledPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var installed = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()) + .OrderBy(record => record.Skill.Name, StringComparer.Ordinal) + .ToArray(); + var outdated = installed.Where(record => !record.IsCurrent).ToArray(); + + var filtered = installed.Where(r => MatchesFilter(r.Skill.Name, r.Skill.Stack, r.Skill.Lane)).ToArray(); + + panel.AddControl(BuildPropertyPanel("installed skills", AccentGreen, + ("target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("installed", string.IsNullOrEmpty(_searchFilter) ? installed.Length.ToString() : $"{filtered.Length}/{installed.Length}"), + ("outdated", outdated.Length == 0 ? "[green]0[/]" : $"[yellow]{outdated.Length}[/]"), + ("tokens", FormatTokenCount(installed.Sum(record => record.Skill.TokenCount))))); + AddSearchChip(panel); + + if (installed.Length == 0) + { + panel.AddControl(BuildNotePanel("installed", "[grey50]No catalog skills are installed in this target yet. Visit the Skills page to add some.[/]", AccentDeepSkyBlue)); + return; + } + if (filtered.Length == 0) + { + panel.AddControl(BuildNotePanel("installed", $"[grey50]No installed skills match “{Escape(_searchFilter)}”.[/]", AccentYellow)); + return; + } + + // Real sortable TableControl — columns can be sorted by clicking the header. Per-row + // foreground color flags outdated rows yellow without needing markup escaping per cell. + var table = Controls.Table() + .WithTitle("Installed skills (Enter for details)") + .AddColumn("Status", TextJustification.Center, width: 8) + .AddColumn("Skill") + .AddColumn("Collection") + .AddColumn("Lane") + .AddColumn("Installed", TextJustification.Right) + .AddColumn("Latest", TextJustification.Right) + .AddColumn("Tokens", TextJustification.Right) + .WithSorting() + .Rounded() + .WithBorderColor(AccentGreen); + foreach (var record in filtered) + { + var row = new TableRow( + record.IsCurrent ? "✓ current" : "↻ update", + ToAlias(record.Skill.Name), + record.Skill.Stack, + record.Skill.Lane, + record.InstalledVersion, + record.Skill.Version, + FormatTokenCount(record.Skill.TokenCount)) + { + Tag = record, + ForegroundColor = record.IsCurrent ? null : AccentYellow, + }; + table.AddRow(row); + } + // RowActivated fires on Enter or double-click; index is into the filtered array because + // we appended rows in the same order. + table.OnRowActivated((_, idx) => + { + if (idx >= 0 && idx < filtered.Length) + { + ShowInstalledSkillModal(ws, panel, filtered[idx]); + } + }); + panel.AddControl(table.Build()); + + if (outdated.Length > 0) + { + panel.AddControl(Controls.Button($"Update all {outdated.Length} outdated skill(s)") + .OnClick((_, _) => + { + var summaryText = UpdateSkillRecords(outdated); + Toast(summaryText, summaryText.Contains("failed", StringComparison.OrdinalIgnoreCase) ? NotificationSeverity.Danger : NotificationSeverity.Success); + BuildInstalledPage(ws, panel); + }).Build()); + } + + panel.AddControl(Controls.Button($"Remove all {installed.Length} installed skill(s)") + .OnClick((_, _) => ConfirmModal(ws, "Remove all installed skills?", + $"This removes every catalog skill from {layout.PrimaryRoot.FullName}.", + () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Remove(installed.Select(r => r.Skill).ToArray(), layout), default(SkillRemoveSummary)); + ToastResult(summary, "Remove failed", summary is null ? string.Empty : $"Removed {summary.RemovedCount} skill(s)"); + BuildInstalledPage(ws, panel); + })).Build()); + } + + private void ShowInstalledSkillModal(ConsoleWindowSystem ws, ScrollablePanelControl owner, InstalledSkillRecord record) + { + var detail = new IWindowControl[] + { + BuildPropertyPanel(ToAlias(record.Skill.Name), AccentGreen, + ("skill", Escape(record.Skill.Name)), + ("collection", Escape($"{record.Skill.Stack} / {record.Skill.Lane}")), + ("installed", Escape(record.InstalledVersion)), + ("latest", Escape(record.Skill.Version)), + ("status", record.IsCurrent ? "[green]✓ current[/]" : "[yellow]↻ update available[/]"), + ("tokens", FormatTokenCount(record.Skill.TokenCount))), + BuildNotePanel("summary", Escape(record.Skill.Description), AccentDeepSkyBlue), + }; + + var buttons = new List<(string, Action)>(); + if (!record.IsCurrent) + { + buttons.Add(($"Update to {record.Skill.Version}", () => + { + var msg = UpdateSkillRecords(new[] { record }); + Toast(msg, msg.Contains("failed", StringComparison.OrdinalIgnoreCase) ? NotificationSeverity.Danger : NotificationSeverity.Success); + BuildInstalledPage(ws, owner); + })); + } + buttons.Add(("Reinstall (force)", () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Install(new[] { record.Skill }, ResolveSkillLayout(), force: true), default(SkillInstallSummary)); + ToastResult(summary, "Reinstall failed", $"{ToAlias(record.Skill.Name)}: reinstalled"); + BuildInstalledPage(ws, owner); + })); + buttons.Add(("Remove", () => ConfirmModal(ws, $"Remove {ToAlias(record.Skill.Name)}?", $"Deletes the skill directory from {ResolveSkillLayout().PrimaryRoot.FullName}.", () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Remove(new[] { record.Skill }, ResolveSkillLayout()), default(SkillRemoveSummary)); + ToastResult(summary, "Remove failed", $"Removed {ToAlias(record.Skill.Name)}"); + BuildInstalledPage(ws, owner); + }))); + + ShowModalNative(ws, $"Installed · {ToAlias(record.Skill.Name)}", detail, buttons.ToArray()); + } + + private string UpdateSkillRecords(IReadOnlyList records) + { + var layout = ResolveSkillLayout(); + var skills = records.Select(record => record.Skill).ToArray(); + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Install(skills, layout, force: true), default(SkillInstallSummary)); + return summary is null ? "Update failed" : $"Updated {summary.InstalledCount} skill(s)"; + } + + // ------------------------------------------------------------------------- + // Collections + // ------------------------------------------------------------------------- + + private void BuildCollectionsPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var installed = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()); + var views = BuildCollectionViews(installed) + .OrderBy(view => CatalogOrganization.GetStackRank(view.Collection)) + .ThenBy(view => view.Collection, StringComparer.Ordinal) + .ToArray(); + var filtered = views.Where(v => MatchesFilter(v.Collection)).ToArray(); + + panel.AddControl(BuildPropertyPanel("collection browser", AccentDeepSkyBlue, + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("collections", string.IsNullOrEmpty(_searchFilter) ? views.Length.ToString() : $"{filtered.Length}/{views.Length}"), + ("skills", skillCatalog.Skills.Count.ToString()), + ("installed", $"{installed.Count}/{skillCatalog.Skills.Count}"))); + AddSearchChip(panel); + + if (views.Length == 0) + { + panel.AddControl(BuildNotePanel("collections", "[grey50]No collections in this catalog version.[/]", AccentDeepSkyBlue)); + return; + } + if (filtered.Length == 0) + { + panel.AddControl(BuildNotePanel("collections", $"[grey50]No collections match “{Escape(_searchFilter)}”.[/]", AccentYellow)); + return; + } + + // Master-detail layout. Left column lists collections; right column shows the detail of + // _selectedCollection. Clicking a left-list row updates only the right pane in place — + // no modal, no full-page rebuild. The right pane is a ScrollablePanel so the detail can + // grow with the collection's lane list. + if (_selectedCollection is null + || !filtered.Any(v => string.Equals(v.Collection, _selectedCollection.Collection, StringComparison.OrdinalIgnoreCase))) + { + _selectedCollection = filtered[0]; + _collectionInstallArmed = false; + } + + // Build the detail pane as a standalone ScrollablePanelControl so we can update it + // independently of the left list when the user changes selection. + var rightPane = new ScrollablePanelControl + { + ShowScrollbar = true, + VerticalScrollMode = ScrollMode.Scroll, + EnableMouseWheel = true, + }; + + var grid = Controls.HorizontalGrid() + .Column(col => + { + col.Flex(1); + var list = StyledList("Collections") + .MaxVisibleItems(20) + .WithScrollbarVisibility(ScrollbarVisibility.Auto); + foreach (var view in filtered) + { + list.AddItem(Escape(BuildCollectionChoiceLabel(view)), view); + } + list.OnItemActivated((_, item) => + { + if (item.Tag is CollectionCatalogView v) + { + _selectedCollection = v; + _collectionInstallArmed = false; + BuildCollectionDetail(rightPane, v); + } + }); + col.Add(list.Build()); + }) + .Column(col => + { + col.Flex(2).Add(rightPane); + }) + .Build(); + + panel.AddControl(grid); + BuildCollectionDetail(rightPane, _selectedCollection!); + } + + /// + /// Renders the right pane of the Collections master-detail view: stats, lanes, and an inline + /// two-stage install button (first click arms, second commits — satisfies AGENTS.md's + /// "install overview before confirmation" rule without a modal). + /// + private void BuildCollectionDetail(ScrollablePanelControl pane, CollectionCatalogView view) + { + pane.ClearContents(); + pane.AddControl(BuildPropertyPanel(view.Collection, AccentDeepSkyBlue, + ("collection", Escape(view.Collection)), + ("lanes", view.Lanes.Count.ToString()), + ("skills", $"{view.InstalledCount}/{view.SkillCount}"), + ("tokens", FormatTokenCount(view.TokenCount)))); + + if (view.Lanes.Count > 0) + { + pane.AddControl(BuildBulletPanel("lanes", AccentTurquoise, + view.Lanes.Select(lane => $"[grey50]·[/] [grey]{Escape(lane.Lane)}[/] [grey50]({lane.InstalledCount}/{lane.Skills.Count} skills, {FormatTokenCount(lane.TokenCount)} tokens)[/]").ToArray())); + } + + var armed = _collectionInstallArmed; + var label = armed + ? $"Click again to install all {view.SkillCount} skill(s)" + : $"Install collection ({view.SkillCount} skill(s))"; + pane.AddControl(Controls.Button(label) + .OnClick((_, _) => + { + if (!_collectionInstallArmed) + { + _collectionInstallArmed = true; + Toast($"Click again to confirm installing {view.SkillCount} skill(s)", NotificationSeverity.Warning); + BuildCollectionDetail(pane, view); + return; + } + var skills = SafeGet(() => new SkillInstaller(skillCatalog).SelectSkillsFromCollections(new[] { view.Collection }), Array.Empty()); + var summary = skills.Count == 0 ? null : SafeGet(() => new SkillInstaller(skillCatalog).Install(skills, ResolveSkillLayout(), force: false), default(SkillInstallSummary)); + ToastResult(summary, $"Could not install collection {view.Collection}", summary is null ? string.Empty : $"{view.Collection}: {summary.InstalledCount} written, {summary.SkippedExisting.Count} skipped"); + _collectionInstallArmed = false; + if (_ws is not null && _activePanel is not null) BuildCollectionsPage(_ws, _activePanel); + }).Build()); + } + + // ------------------------------------------------------------------------- + // Bundles / packages + // ------------------------------------------------------------------------- + + private void BuildBundlesPage(ConsoleWindowSystem ws, ScrollablePanelControl panel, bool primaryOnly) + { + panel.ClearContents(); + + var packages = (primaryOnly + ? GetPrimaryBundles() + : skillCatalog.Packages.OrderBy(p => p.Name, StringComparer.Ordinal).ToArray()) + .ToArray(); + var title = primaryOnly ? "focused bundles" : "catalog packages"; + var skillTokens = skillCatalog.Skills.ToDictionary(skill => skill.Name, skill => skill.TokenCount, StringComparer.OrdinalIgnoreCase); + + var filtered = packages.Where(p => MatchesFilter(p.Name, p.Title)).ToArray(); + + panel.AddControl(BuildPropertyPanel(title, AccentDeepSkyBlue, + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + (primaryOnly ? "bundles" : "packages", string.IsNullOrEmpty(_searchFilter) ? packages.Length.ToString() : $"{filtered.Length}/{packages.Length}"), + ("skills covered", skillCatalog.Skills.Count.ToString()))); + AddSearchChip(panel); + + if (packages.Length == 0) + { + panel.AddControl(BuildNotePanel(title, "[grey50]Nothing available in this catalog version.[/]", AccentDeepSkyBlue)); + return; + } + if (filtered.Length == 0) + { + panel.AddControl(BuildNotePanel(title, $"[grey50]No bundles match “{Escape(_searchFilter)}”.[/]", AccentYellow)); + return; + } + + var list = StyledList($"{(primaryOnly ? "Bundles" : "Packages")} (Enter for details)") + .MaxVisibleItems(16) + .WithScrollbarVisibility(ScrollbarVisibility.Auto); + foreach (var package in filtered) + { + var tokenCount = package.Skills.Sum(name => skillTokens.TryGetValue(name, out var value) ? value : 0); + list.AddItem($"{Escape(package.Name)} [dim]({package.Skills.Count} skills, {FormatTokenCount(tokenCount)} tokens)[/]", package); + } + list.OnItemActivated((_, item) => + { + if (item.Tag is SkillPackageEntry package) + { + ShowBundleModal(ws, panel, package, primaryOnly); + } + }); + panel.AddControl(list.Build()); + } + + private void ShowBundleModal(ConsoleWindowSystem ws, ScrollablePanelControl owner, SkillPackageEntry package, bool primaryOnly) + { + var detail = new IWindowControl[] + { + BuildPropertyPanel(package.Name, AccentTurquoise, + ("package", Escape(package.Name)), + ("title", Escape(package.Title)), + ("skills", package.Skills.Count.ToString()), + ("includes", Escape(string.Join(", ", package.Skills.Take(10).Select(ToAlias))))), + BuildNotePanel("summary", Escape(package.Description), AccentDeepSkyBlue), + }; + + ShowModalNative(ws, $"Bundle · {package.Name}", detail, + ("Install bundle into current target", () => + { + var skills = SafeGet(() => new SkillInstaller(skillCatalog).SelectSkillsFromPackages(new[] { package.Name }), Array.Empty()); + var summary = skills.Count == 0 ? null : SafeGet(() => new SkillInstaller(skillCatalog).Install(skills, ResolveSkillLayout(), force: false), default(SkillInstallSummary)); + ToastResult(summary, $"Could not install bundle {package.Name}", summary is null ? string.Empty : $"{package.Name}: {summary.InstalledCount} written, {summary.SkippedExisting.Count} skipped"); + BuildBundlesPage(ws, owner, primaryOnly); + })); + } + + // ------------------------------------------------------------------------- + // Packages — NuGet ids / prefixes → catalog skills + // ------------------------------------------------------------------------- + + private void BuildPackagesPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var signals = SafeGet(BuildPackageSignals, Array.Empty()); + var filtered = signals.Where(s => MatchesFilter(s.Signal, s.Skill.Name, s.Skill.Stack, s.Skill.Lane)).ToArray(); + + panel.AddControl(BuildPropertyPanel("package signals", AccentTurquoise, + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("signals", string.IsNullOrEmpty(_searchFilter) ? signals.Count.ToString() : $"{filtered.Length}/{signals.Count}"), + ("skills covered", signals.Select(s => s.Skill.Name).Distinct(StringComparer.OrdinalIgnoreCase).Count().ToString()))); + AddSearchChip(panel); + + if (signals.Count == 0) + { + panel.AddControl(BuildNotePanel("packages", "[grey50]No NuGet package or prefix signals are present in this catalog version.[/]", AccentDeepSkyBlue)); + return; + } + if (filtered.Length == 0) + { + panel.AddControl(BuildNotePanel("packages", $"[grey50]No signals match “{Escape(_searchFilter)}”.[/]", AccentYellow)); + return; + } + + var list = StyledList("Package signals (Enter to inspect linked skill)") + .MaxVisibleItems(16) + .WithScrollbarVisibility(ScrollbarVisibility.Auto); + foreach (var signal in filtered) + { + // ListControl renders item text as markup — escape the whole plain-text label. + list.AddItem(Escape($"{signal.Signal} [{signal.Kind}] -> {ToAlias(signal.Skill.Name)} [{signal.Skill.Stack} / {signal.Skill.Lane}] ({FormatTokenCount(signal.Skill.TokenCount)} tokens)"), signal); + } + list.OnItemActivated((_, item) => + { + if (item.Tag is PackageSignalView signal) + { + ShowSkillDetailModal(ws, panel, signal.Skill); + } + }); + panel.AddControl(list.Build()); + } + + // ------------------------------------------------------------------------- + // Agents + // ------------------------------------------------------------------------- + + private void BuildAgentsPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = TryResolveAgentLayout(out var layoutError); + var installer = new AgentInstaller(agentCatalog); + var installed = layout is null + ? Array.Empty() + : SafeGet(() => installer.GetInstalledAgents(layout), Array.Empty()); + + var allAgents = agentCatalog.Agents.OrderBy(a => a.Name, StringComparer.Ordinal).ToArray(); + var filteredAgents = allAgents.Where(a => MatchesFilter(a.Name, a.Description)).ToArray(); + + panel.AddControl(BuildPropertyPanel("orchestration agents", AccentMediumPurple, + ("agents", string.IsNullOrEmpty(_searchFilter) ? agentCatalog.Agents.Count.ToString() : $"{filteredAgents.Length}/{agentCatalog.Agents.Count}"), + ("platform", Escape(Session.Agent.ToString())), + ("target", layout is null ? $"[red]{Escape(layoutError ?? "unresolved")}[/]" : $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("installed", layout is null ? "[grey]-[/]" : $"{installed.Count}/{agentCatalog.Agents.Count}"))); + AddSearchChip(panel); + + if (agentCatalog.Agents.Count == 0) + { + panel.AddControl(BuildNotePanel("agents", "[grey50]No agents available in the catalog.[/]", AccentDeepSkyBlue)); + return; + } + if (filteredAgents.Length == 0) + { + panel.AddControl(BuildNotePanel("agents", $"[grey50]No agents match “{Escape(_searchFilter)}”.[/]", AccentYellow)); + return; + } + + var list = StyledList("Agents (Enter for details)") + .MaxVisibleItems(14) + .WithScrollbarVisibility(ScrollbarVisibility.Auto); + foreach (var agent in filteredAgents) + { + var isInstalled = installed.Any(i => string.Equals(i.Agent.Name, agent.Name, StringComparison.OrdinalIgnoreCase)); + list.AddItem($"{(isInstalled ? "✓ " : "○ ")}{Escape(ToAlias(agent.Name))} [dim]{Escape(CompactDescription(agent.Description))}[/]", agent); + } + list.OnItemActivated((_, item) => + { + if (item.Tag is AgentEntry agent) + { + ShowAgentModal(ws, panel, agent); + } + }); + panel.AddControl(list.Build()); + + if (layout is null) + { + panel.AddControl(BuildNotePanel("note", "[yellow]No native agent directory resolved. Set the platform on the Settings page, or create one of .codex/.claude/.github/.gemini/.junie.[/]", AccentYellow)); + return; + } + + panel.AddControl(Controls.Button("Install all agents into detected native directories") + .OnClick((_, _) => + { + var detected = SafeGet(() => AgentInstallTarget.ResolveAllDetected(Session.ProjectDirectory, Session.Scope), Array.Empty()); + if (detected.Count == 0) + { + Toast("No native agent directories detected", NotificationSeverity.Warning); + return; + } + var summary2 = SafeGet(() => new AgentInstaller(agentCatalog).InstallToMultiple(agentCatalog.Agents, detected, force: false), default(AgentInstallSummary)); + ToastResult(summary2, "Install failed", summary2 is null ? string.Empty : $"Installed {summary2.InstalledCount} agent file(s) across {detected.Count} platform(s)"); + BuildAgentsPage(ws, panel); + }).Build()); + } + + private void ShowAgentModal(ConsoleWindowSystem ws, ScrollablePanelControl owner, AgentEntry agent) + { + var detail = new IWindowControl[] + { + BuildPropertyPanel(ToAlias(agent.Name), AccentMediumPurple, + ("agent", Escape(agent.Name)), + ("skills", agent.Skills.Count == 0 ? "[grey50]-[/]" : Escape(string.Join(", ", agent.Skills.Select(ToAlias)))), + ("platform", Escape(Session.Agent.ToString()))), + BuildNotePanel("summary", Escape(agent.Description), AccentDeepSkyBlue), + }; + + var buttons = new List<(string, Action)>(); + var layout = TryResolveAgentLayout(out _); + if (layout is not null) + { + buttons.Add(("Install into current target", () => + { + var summary = SafeGet(() => new AgentInstaller(agentCatalog).Install(new[] { agent }, layout, force: false), default(AgentInstallSummary)); + ToastResult(summary, "Install failed", summary is null ? string.Empty : $"{ToAlias(agent.Name)}: {summary.InstalledCount} written, {summary.SkippedExisting.Count} skipped"); + BuildAgentsPage(ws, owner); + })); + buttons.Add(("Remove from current target", () => + { + var summary = SafeGet(() => new AgentInstaller(agentCatalog).Remove(new[] { agent }, layout), default(AgentRemoveSummary)); + ToastResult(summary, "Remove failed", summary is null ? string.Empty : $"Removed {ToAlias(agent.Name)} ({summary.RemovedCount} file(s))"); + BuildAgentsPage(ws, owner); + })); + } + + ShowModalNative(ws, $"Agent · {ToAlias(agent.Name)}", detail, buttons.ToArray()); + } + + // ------------------------------------------------------------------------- + // Project sync / recommend + // ------------------------------------------------------------------------- + + private void BuildProjectPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var scan = SafeGet(() => new ProjectSkillRecommender(skillCatalog).Analyze(Session.ProjectDirectory), null); + if (scan is null) + { + panel.AddControl(BuildNotePanel("project scan", "[red]Could not scan the project directory.[/]", new Color(200, 60, 60))); + return; + } + + var installer = new SkillInstaller(skillCatalog); + var installedByName = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()) + .ToDictionary(record => record.Skill.Name, StringComparer.OrdinalIgnoreCase); + + var high = scan.Recommendations.Count(r => r.Confidence == RecommendationConfidence.High); + var med = scan.Recommendations.Count(r => r.Confidence == RecommendationConfidence.Medium); + var low = scan.Recommendations.Count(r => r.Confidence == RecommendationConfidence.Low); + + panel.AddControl(BuildPropertyPanel("project scan", AccentDeepSkyBlue, + ("project", $"[grey50]{Escape(CompactPath(scan.ProjectRoot.FullName))}[/]"), + ("scanned", $"{scan.ProjectFiles.Count} project file(s)"), + ("frameworks", scan.TargetFrameworks.Count == 0 ? "[grey50]unknown[/]" : Escape(string.Join(", ", scan.TargetFrameworks))), + ("target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("recommendations", $"{scan.Recommendations.Count} [grey50]([/][green]{high} high[/][grey50] · [/][yellow]{med} med[/][grey50] · [/][grey]{low} low[/][grey50])[/]"))); + + if (scan.Recommendations.Count == 0) + { + panel.AddControl(BuildNotePanel("recommendations", "[grey50]No package or framework signals matched the catalog. Start with the[/] [green]dotnet[/] [grey50]and[/] [green]modern-csharp[/] [grey50]skills from the Skills page.[/]", AccentDeepSkyBlue)); + return; + } + + var list = StyledList("Recommended skills (Enter to install)") + .MaxVisibleItems(16) + .WithScrollbarVisibility(ScrollbarVisibility.Auto); + foreach (var recommendation in scan.Recommendations + .OrderByDescending(r => r.Confidence) + .ThenBy(r => r.Skill.Name, StringComparer.Ordinal)) + { + var marker = recommendation.Confidence switch + { + RecommendationConfidence.High => "[green]●●●[/]", + RecommendationConfidence.Medium => "[yellow]●●○[/]", + _ => "[grey]●○○[/]", + }; + installedByName.TryGetValue(recommendation.Skill.Name, out var record); + var status = record is null ? "[deepskyblue1]new[/]" : record.IsCurrent ? "[green]installed[/]" : "[yellow]update[/]"; + list.AddItem($"{marker} {Escape(ToAlias(recommendation.Skill.Name))} [dim]{status}[/] [grey]{Escape(string.Join("; ", recommendation.Reasons.Take(2)))}[/]", recommendation); + } + list.OnItemActivated((_, item) => + { + if (item.Tag is ProjectSkillRecommendation recommendation) + { + // Outdated recommendations need force=true: SkillInstaller.Install skips + // existing skill directories unless forced, so an "update" entry would + // otherwise be reported as skipped and stay outdated. + var isOutdated = installedByName.TryGetValue(recommendation.Skill.Name, out var existing) && !existing.IsCurrent; + var summary2 = SafeGet(() => new SkillInstaller(skillCatalog).Install(new[] { recommendation.Skill }, ResolveSkillLayout(), force: isOutdated), default(SkillInstallSummary)); + ToastResult(summary2, $"Install failed for {ToAlias(recommendation.Skill.Name)}", summary2 is null ? string.Empty : $"{ToAlias(recommendation.Skill.Name)}: {summary2.InstalledCount} written, {summary2.SkippedExisting.Count} skipped"); + BuildProjectPage(ws, panel); + } + }); + panel.AddControl(list.Build()); + + // Split recommendations: new ones install with force=false, outdated ones need + // force=true so the existing skill directory is overwritten with the latest version. + var newSkills = scan.Recommendations + .Where(r => !installedByName.ContainsKey(r.Skill.Name)) + .Select(r => r.Skill) + .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()) + .ToArray(); + var outdatedSkills = scan.Recommendations + .Where(r => installedByName.TryGetValue(r.Skill.Name, out var rec) && !rec.IsCurrent) + .Select(r => r.Skill) + .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()) + .ToArray(); + var installable = newSkills.Concat(outdatedSkills).ToArray(); + if (installable.Length > 0) + { + panel.AddControl(Controls.Button($"Install all {installable.Length} recommended skill(s)") + .OnClick((_, _) => + { + var skillLayout = ResolveSkillLayout(); + var installer2 = new SkillInstaller(skillCatalog); + var newSummary = newSkills.Length == 0 ? default : SafeGet(() => installer2.Install(newSkills, skillLayout, force: false), default(SkillInstallSummary)); + var updateSummary = outdatedSkills.Length == 0 ? default : SafeGet(() => installer2.Install(outdatedSkills, skillLayout, force: true), default(SkillInstallSummary)); + var installedCount = (newSummary?.InstalledCount ?? 0) + (updateSummary?.InstalledCount ?? 0); + var skippedCount = (newSummary?.SkippedExisting.Count ?? 0) + (updateSummary?.SkippedExisting.Count ?? 0); + var failed = installedCount == 0 && skippedCount == 0; + Toast(failed ? "Install failed" : $"Installed {installedCount}, skipped {skippedCount}", failed ? NotificationSeverity.Danger : NotificationSeverity.Success); + BuildProjectPage(ws, panel); + }).Build()); + } + } + + // ------------------------------------------------------------------------- + // Catalog analysis + // ------------------------------------------------------------------------- + + private void BuildAnalysisPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var installed = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()); + var views = BuildCollectionViews(installed) + .OrderByDescending(view => view.SkillCount) + .ToArray(); + var signals = SafeGet(BuildPackageSignals, Array.Empty()); + var heaviest = skillCatalog.Skills.OrderByDescending(skill => skill.TokenCount).Take(12).ToArray(); + + panel.AddControl(BuildPropertyPanel("catalog analysis", AccentDeepSkyBlue, + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("collections", views.Length.ToString()), + ("skills", skillCatalog.Skills.Count.ToString()), + ("total tokens", FormatTokenCount(skillCatalog.Skills.Sum(skill => skill.TokenCount))), + ("package signals", signals.Count.ToString()))); + + var collectionCards = views.Take(12).Select(view => BuildBulletPanel( + view.Collection, AccentDeepSkyBlue, + $"[grey50]skills[/] {view.SkillCount} [grey50]installed[/] {view.InstalledCount} [grey50]tokens[/] {FormatTokenCount(view.TokenCount)}")).ToList(); + panel.AddControl(BuildCardGrid(collectionCards, maxColumns: 3)); + + var heavyTable = Controls.Table() + .WithTitle("Heaviest skills (Enter for details)") + .AddColumn("Skill") + .AddColumn("Collection") + .AddColumn("Lane") + .AddColumn("Tokens", TextJustification.Right) + .WithSorting() + .Rounded() + .WithBorderColor(AccentDeepSkyBlue); + foreach (var skill in heaviest) + { + heavyTable.AddRow(new TableRow(ToAlias(skill.Name), skill.Stack, skill.Lane, FormatTokenCount(skill.TokenCount)) { Tag = skill }); + } + heavyTable.OnRowActivated((_, idx) => + { + if (idx >= 0 && idx < heaviest.Length) + { + ShowSkillDetailModal(ws, panel, heaviest[idx]); + } + }); + panel.AddControl(heavyTable.Build()); + + // Native bar charts: skills sorted by tokens (heaviest 12), then collections sorted by + // skill count (top 8). Each bar uses the standard threshold gradient so the eye picks + // up "big" entries immediately. + if (heaviest.Length > 0) + { + var maxTokens = heaviest.Max(s => s.TokenCount); + var chart1 = new ScrollablePanelControl + { + ShowScrollbar = false, + EnableMouseWheel = false, + }; + foreach (var skill in heaviest) + { + chart1.AddControl(BuildSkillTokenBar(skill, maxTokens)); + } + panel.AddControl(BuildSectionPanel("tokens by skill (top 12)", string.Empty, AccentDeepSkyBlue)); + panel.AddControl(chart1); + } + + var topCollections = views.Take(8).ToArray(); + if (topCollections.Length > 0) + { + var maxCount = topCollections.Max(v => v.SkillCount); + var chart2 = new ScrollablePanelControl + { + ShowScrollbar = false, + EnableMouseWheel = false, + }; + foreach (var view in topCollections) + { + chart2.AddControl(BuildCollectionCountBar(view, maxCount)); + } + panel.AddControl(BuildSectionPanel("skills per collection (top 8)", string.Empty, AccentTurquoise)); + panel.AddControl(chart2); + } + + if (signals.Count > 0) + { + var signalLines = signals.Take(18).Select(signal => + $"[grey]{Escape(signal.Signal)}[/] [grey50]({Escape(signal.Kind)})[/] [grey50]→[/] {Escape(ToAlias(signal.Skill.Name))}").ToArray(); + panel.AddControl(BuildBulletPanel("package signals", AccentTurquoise, signalLines)); + } + } + + /// + /// A horizontal bar showing one skill's token weight against the chart's max. Color follows + /// a green→yellow→red threshold gradient so heavy skills stand out visually. + /// + private static BarGraphControl BuildSkillTokenBar(SkillEntry skill, int maxTokens) + => Controls.BarGraph() + .WithLabel($"{ToAlias(skill.Name)}") + .WithLabelWidth(28) + .WithValue(skill.TokenCount) + .WithMaxValue(maxTokens == 0 ? 1 : maxTokens) + .WithValueFormat("N0") + .ShowValue(true) + .WithStandardGradient() + .Build(); + + /// + /// A horizontal bar showing one collection's skill count against the chart's max. Uses the + /// turquoise accent for the filled portion. + /// + private static BarGraphControl BuildCollectionCountBar(CollectionCatalogView view, int maxCount) + => Controls.BarGraph() + .WithLabel(view.Collection) + .WithLabelWidth(28) + .WithValue(view.SkillCount) + .WithMaxValue(maxCount == 0 ? 1 : maxCount) + .WithValueFormat("0") + .ShowValue(true) + .WithFilledColor(AccentTurquoise) + .Build(); + + // ------------------------------------------------------------------------- + // Remove all / Update all action pages + // ------------------------------------------------------------------------- + + private void BuildRemoveAllPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var installed = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()); + + panel.AddControl(BuildPropertyPanel("remove all installed skills", new Color(200, 60, 60), + ("target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("installed", installed.Count.ToString()))); + + if (installed.Count == 0) + { + panel.AddControl(BuildNotePanel("status", "[grey50]Nothing to remove in this target.[/]", AccentDeepSkyBlue)); + return; + } + + panel.AddControl(Controls.Button($"Remove all {installed.Count} skill(s) from this target") + .OnClick((_, _) => ConfirmModal(ws, "Remove all installed skills?", $"Deletes every catalog skill directory under {layout.PrimaryRoot.FullName}.", () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Remove(installed.Select(r => r.Skill).ToArray(), layout), default(SkillRemoveSummary)); + ToastResult(summary, "Remove failed", summary is null ? string.Empty : $"Removed {summary.RemovedCount} skill(s)"); + BuildRemoveAllPage(ws, panel); + })).Build()); + } + + private void BuildUpdateAllPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + var layout = ResolveSkillLayout(); + var installer = new SkillInstaller(skillCatalog); + var outdated = SafeGet(() => installer.GetInstalledSkills(layout), Array.Empty()) + .Where(record => !record.IsCurrent) + .ToArray(); + + panel.AddControl(BuildPropertyPanel("update all outdated skills", AccentYellow, + ("target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("outdated", outdated.Length.ToString()))); + + if (outdated.Length == 0) + { + panel.AddControl(BuildNotePanel("status", "[green]All installed skills already match the catalog version.[/]", AccentGreen)); + return; + } + + var pendingLines = outdated.Select(record => + $"[yellow]↻[/] {Escape(ToAlias(record.Skill.Name))} [grey50]{Escape(record.InstalledVersion)} → {Escape(record.Skill.Version)}[/]").ToArray(); + panel.AddControl(BuildBulletPanel("pending updates", AccentYellow, pendingLines)); + + panel.AddControl(Controls.Button($"Update all {outdated.Length} skill(s)") + .OnClick((_, _) => + { + var msg = UpdateSkillRecords(outdated); + Toast(msg, msg.Contains("failed", StringComparison.OrdinalIgnoreCase) ? NotificationSeverity.Danger : NotificationSeverity.Success); + BuildUpdateAllPage(ws, panel); + }).Build()); + } + + // ------------------------------------------------------------------------- + // Settings / workspace + // ------------------------------------------------------------------------- + + private void BuildSettingsPage(ConsoleWindowSystem ws, ScrollablePanelControl panel) + { + panel.ClearContents(); + + var layout = ResolveSkillLayout(); + var agentStatus = ResolveAgentStatus(); + panel.AddControl(BuildPropertyPanel("workspace", AccentDeepSkyBlue, + ("platform", Escape(Session.Agent.ToString())), + ("scope", Escape(Session.Scope.ToString())), + ("project", Escape(CompactPath(Session.ProjectDirectory ?? Environment.CurrentDirectory))), + ("skill target", $"[grey50]{Escape(CompactPath(layout.PrimaryRoot.FullName))}[/]"), + ("agent target", agentStatus.Layout is null ? $"[red]{Escape(agentStatus.Summary)}[/]" : $"[grey50]{Escape(CompactPath(agentStatus.Layout.PrimaryRoot.FullName))}[/]"), + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("build", ToolVersionInfo.IsDevelopmentBuild ? "[grey50]local development[/]" : "[green]published[/]"))); + + // Inline form: native dropdowns (change-on-pick, no modal) for Platform/Scope, + // a plain Button for catalog refresh. SelectedIndexChanged fires only on user + // interaction (DropdownBuilder attaches the handler AFTER SelectedIndex is set), + // so no guard flag is needed against the initial-paint pulse. + var platformValues = Enum.GetValues(); + var platformDropdown = Controls.Dropdown("Platform") + .AddItems(platformValues.Select(v => v.ToString()).ToArray()) + .SelectedIndex(Array.IndexOf(platformValues, Session.Agent)) + .OnSelectionChanged((_, idx) => + { + if (idx < 0 || idx >= platformValues.Length) return; + var chosen = platformValues[idx]; + if (chosen.Equals(Session.Agent)) return; + Session.Agent = chosen; + Toast($"Platform set to {chosen}", NotificationSeverity.Success); + }) + .Build(); + + var scopeValues = Enum.GetValues(); + var scopeDropdown = Controls.Dropdown("Scope") + .AddItems(scopeValues.Select(v => v.ToString()).ToArray()) + .SelectedIndex(Array.IndexOf(scopeValues, Session.Scope)) + .OnSelectionChanged((_, idx) => + { + if (idx < 0 || idx >= scopeValues.Length) return; + var chosen = scopeValues[idx]; + if (chosen.Equals(Session.Scope)) return; + Session.Scope = chosen; + Toast($"Scope set to {chosen}", NotificationSeverity.Success); + }) + .Build(); + + panel.AddControl(BuildSectionPanel("install target", "[grey50]Platform and scope control where skills and agents are written. Changes take effect immediately.[/]", AccentDeepSkyBlue)); + panel.AddControl(platformDropdown); + panel.AddControl(scopeDropdown); + + panel.AddControl(BuildSectionPanel("catalog", "[grey50]Pull the latest catalog from upstream.[/]", AccentTurquoise)); + panel.AddControl(Controls.Button("Refresh catalog now") + .OnClick((_, _) => RefreshCatalogFromUi()) + .Build()); + } + + // ------------------------------------------------------------------------- + // About + // ------------------------------------------------------------------------- + + private void BuildAboutPage(ScrollablePanelControl panel) + { + panel.ClearContents(); + panel.AddControl(BuildPropertyPanel("about", AccentDeepSkyBlue, + ("tool", $"{Escape(ToolIdentity.DisplayCommand)}"), + ("package", Escape(ToolIdentity.PackageId)), + ("version", Escape(ToolVersionInfo.CurrentVersion)), + ("build", ToolVersionInfo.IsDevelopmentBuild ? "[grey50]local development[/]" : "[green]published[/]"), + ("catalog", $"{Escape(skillCatalog.SourceLabel)} [grey50]({Escape(skillCatalog.CatalogVersion)})[/]"), + ("skills", skillCatalog.Skills.Count.ToString()), + ("agents", agentCatalog.Agents.Count.ToString()))); + + panel.AddControl(BuildBulletPanel("surface map", AccentDeepSkyBlue, + "[grey]Home[/] [grey50]session, catalog telemetry, update notice[/]", + "[grey]Skills / Installed[/] [grey50]browse, install, update, remove catalog skills[/]", + "[grey]Collections / Bundles / Packages[/] [grey50]install grouped surfaces[/]", + "[grey]Agents[/] [grey50]install orchestration agents into native agent directories[/]", + "[grey]Project[/] [grey50]scan .csproj signals and install recommended skills[/]", + "[grey]Analysis[/] [grey50]collection sizes, heaviest skills, package signals[/]")); + + panel.AddControl(BuildBulletPanel("notes", AccentGrey, + "[grey50]This is the SharpConsoleUI command center. Run with redirected stdin/stdout to get the classic prompt shell instead.[/]", + "[grey50]CLI sub-commands (list, install, recommend, …) are unchanged — see[/] [green]dotnet skills help[/][grey50].[/]")); + } + + // ------------------------------------------------------------------------- + // Modal + status helpers + // ------------------------------------------------------------------------- + + private void ShowModalNative(ConsoleWindowSystem ws, string title, IReadOnlyList contents, params (string Label, Action OnClick)[] buttons) + { + Window? modal = null; + var width = Math.Clamp(SafeConsole(() => Console.WindowWidth, 120) - 10, 56, 116); + var height = Math.Clamp(SafeConsole(() => Console.WindowHeight, 32) - 6, 14, 34); + + var body = Controls.ScrollablePanel().Build(); + foreach (var c in contents) + { + body.AddControl(c); + } + + void Close() + { + if (modal is not null) + { + ws.CloseWindow(modal); + } + } + + var toolbar = Controls.Toolbar().WithSpacing(2).WithAlignment(HorizontalAlignment.Center); + foreach (var (label, onClick) in buttons) + { + var captured = onClick; + toolbar.AddButton(label, (_, _) => { Close(); captured(); }); + } + toolbar.AddButton("Close", (_, _) => Close()); + + modal = new WindowBuilder(ws) + .WithTitle(title) + .WithSize(width, height) + .Centered() + .AsModal() + .Minimizable(false) + .Maximizable(false) + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(new Color(90, 110, 142)) + .OnKeyPressed((_, e) => + { + if (e.KeyInfo.Key == ConsoleKey.Escape) + { + Close(); + e.Handled = true; + } + }) + .AddControl(body) + .AddControl(toolbar.StickyBottom().Build()) + .BuildAndShow(); + } + + private void ConfirmModal(ConsoleWindowSystem ws, string title, string message, Action onConfirm) + { + var content = new IWindowControl[] + { + BuildNotePanel("confirm", $"[yellow]{Escape(message)}[/]", AccentYellow), + }; + ShowModalNative(ws, title, content, ("Yes, proceed", onConfirm)); + } + + private void ChooseEnumModal(ConsoleWindowSystem ws, string title, TEnum[] values, TEnum current, Action onPicked) + where TEnum : struct, Enum + { + Window? modal = null; + + void Close() + { + if (modal is not null) + { + ws.CloseWindow(modal); + } + } + + var list = StyledList(title).MaxVisibleItems(Math.Min(values.Length, 10)); + foreach (var value in values) + { + list.AddItem((value.Equals(current) ? "● " : " ") + value, value); + } + list.OnItemActivated((_, item) => + { + Close(); + if (item.Tag is TEnum picked) + { + onPicked(picked); + } + }); + + var toolbar = Controls.Toolbar().WithSpacing(2).WithAlignment(HorizontalAlignment.Center); + toolbar.AddButton("Cancel", (_, _) => Close()); + + modal = new WindowBuilder(ws) + .WithTitle(title) + .WithSize(Math.Clamp(values.Length == 0 ? 40 : values.Max(v => v.ToString().Length) + 24, 40, 70), Math.Min(values.Length + 8, 18)) + .Centered() + .AsModal() + .Minimizable(false) + .Maximizable(false) + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(new Color(90, 110, 142)) + .OnKeyPressed((_, e) => + { + if (e.KeyInfo.Key == ConsoleKey.Escape) + { + Close(); + e.Handled = true; + } + }) + .AddControl(list.Build()) + .AddControl(toolbar.StickyBottom().Build()) + .BuildAndShow(); + } + + /// + /// Opens a small modal hosting a PromptControl. Pressing Enter sets the active page's + /// _searchFilter and rebuilds it. Esc dismisses without changing the filter. Triggered by + /// `/` from any list-bearing page. + /// + private void ShowSearchOverlay() + { + if (_ws is null) return; + Window? modal = null; + + void Close() + { + if (modal is not null) _ws.CloseWindow(modal); + } + + var prompt = Controls.Prompt($" / ") + .UnfocusOnEnter(false) + .OnEntered((_, query) => + { + _searchFilter = (query ?? string.Empty).Trim(); + Close(); + RebuildActivePage(); + }) + .Build(); + + var hint = new MarkupControl(new List + { + "[grey50]Type to filter the current list. [bold]Enter[/] applies, [bold]Esc[/] cancels.[/]", + string.IsNullOrEmpty(_searchFilter) ? string.Empty : $"[grey50]current:[/] [yellow]{Escape(_searchFilter)}[/]", + }); + + modal = new WindowBuilder(_ws) + .WithTitle("search") + .WithSize(Math.Clamp(SafeConsole(() => Console.WindowWidth, 100) - 20, 50, 80), 9) + .Centered() + .AsModal() + .Minimizable(false) + .Maximizable(false) + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(AccentYellow) + .OnKeyPressed((_, e) => + { + if (e.KeyInfo.Key == ConsoleKey.Escape) + { + Close(); + e.Handled = true; + } + }) + .AddControl(hint) + .AddControl(prompt) + .BuildAndShow(); + } + + /// + /// Global command palette (Ctrl+P). Opens a centered modal hosting a PromptControl + a + /// ListControl pre-populated with every catalog skill, bundle, and agent. Enter on the prompt + /// filters the list; Enter on the list activates the entry (skill/bundle/agent detail modal). + /// + private void ShowCommandPalette(ConsoleWindowSystem ws) + { + Window? modal = null; + var allEntries = BuildPaletteEntries(); + + void Close() + { + if (modal is not null) ws.CloseWindow(modal); + } + + var listControl = StyledList(null).MaxVisibleItems(14).WithScrollbarVisibility(ScrollbarVisibility.Auto).Build(); + listControl.ItemActivated += (_, item) => + { + if (item.Tag is PaletteEntry entry) + { + Close(); + entry.Activate(); + } + }; + void FillList(string filter) + { + listControl.ClearItems(); + var f = filter.Trim(); + var matches = string.IsNullOrEmpty(f) + ? allEntries + : allEntries.Where(e => e.SearchHaystack.Contains(f, StringComparison.OrdinalIgnoreCase)).ToArray(); + foreach (var entry in matches.Take(80)) + { + listControl.AddItem(new ListItem($"[{entry.AccentMarkup}]{entry.IconLabel}[/] {Escape(entry.Label)} [grey50]{Escape(entry.Detail)}[/]") { Tag = entry }); + } + } + + FillList(string.Empty); + + var prompt = Controls.Prompt(" > ") + .UnfocusOnEnter(false) + .OnEntered((_, query) => + { + FillList(query ?? string.Empty); + }) + .Build(); + + modal = new WindowBuilder(ws) + .WithTitle("command palette · Esc to close") + .WithSize(Math.Clamp(SafeConsole(() => Console.WindowWidth, 120) - 10, 64, 100), 22) + .Centered() + .AsModal() + .Minimizable(false) + .Maximizable(false) + .WithBorderStyle(BorderStyle.Rounded) + .WithBorderColor(AccentDeepSkyBlue) + .OnKeyPressed((_, e) => + { + if (e.KeyInfo.Key == ConsoleKey.Escape) + { + Close(); + e.Handled = true; + } + }) + .AddControl(prompt) + .AddControl(listControl) + .BuildAndShow(); + } + + /// + /// Builds the union of every searchable entry — skills, bundles, agents, settings actions — + /// used to populate the command palette. Each entry knows how to activate itself. + /// + private IReadOnlyList BuildPaletteEntries() + { + var entries = new List(); + + foreach (var skill in skillCatalog.Skills) + { + entries.Add(new PaletteEntry( + IconLabel: "◇ skill", + AccentMarkup: "turquoise2", + Label: ToAlias(skill.Name), + Detail: $"{skill.Stack} / {skill.Lane}", + SearchHaystack: $"{skill.Name} {skill.Stack} {skill.Lane}", + Activate: () => { if (_ws is not null && _activePanel is not null) ShowSkillDetailModal(_ws, _activePanel, skill); })); + } + + foreach (var bundle in skillCatalog.Packages) + { + var b = bundle; + entries.Add(new PaletteEntry( + IconLabel: "□ bundle", + AccentMarkup: "springgreen3", + Label: b.Name, + Detail: $"{b.Skills.Count} skill(s)", + SearchHaystack: $"{b.Name} {b.Title} bundle package", + Activate: () => { if (_ws is not null && _activePanel is not null) ShowBundleModal(_ws, _activePanel, b, primaryOnly: false); })); + } + + foreach (var agent in agentCatalog.Agents) + { + var a = agent; + entries.Add(new PaletteEntry( + IconLabel: "△ agent", + AccentMarkup: "mediumpurple2", + Label: ToAlias(a.Name), + Detail: CompactDescription(a.Description), + SearchHaystack: $"{a.Name} agent orchestration {a.Description}", + Activate: () => { if (_ws is not null && _activePanel is not null) ShowAgentModal(_ws, _activePanel, a); })); + } + + // Settings actions and page jumps. + entries.Add(new PaletteEntry("⚙ settings", "deepskyblue1", "Settings", "open workspace settings", "settings platform scope refresh workspace", () => NavigateTo(HomeAction.Workspace))); + entries.Add(new PaletteEntry("↻ refresh", "deepskyblue1", "Refresh catalog", "pull the latest catalog", "refresh catalog reload pull", () => RefreshCatalogFromUi())); + entries.Add(new PaletteEntry("◈ home", "deepskyblue1", "Home", "session and telemetry", "home session telemetry", () => { if (_ws is not null && _activePanel is not null) BuildHomePage(_ws, _activePanel); })); + entries.Add(new PaletteEntry("→ skills", "turquoise2", "Skills", "browse the catalog", "skills browse catalog", () => NavigateTo(HomeAction.BrowseSkills))); + entries.Add(new PaletteEntry("→ installed","green", "Installed", "manage installed skills", "installed manage update remove", () => NavigateTo(HomeAction.ManageInstalled))); + entries.Add(new PaletteEntry("→ collections","deepskyblue1","Collections", "browse collections", "collections browse", () => NavigateTo(HomeAction.BrowseCollections))); + entries.Add(new PaletteEntry("→ bundles", "springgreen3", "Bundles", "focused bundles", "bundles focused", () => NavigateTo(HomeAction.BrowseBundles))); + entries.Add(new PaletteEntry("→ packages", "turquoise2", "Packages", "NuGet signals", "packages nuget signals", () => NavigateTo(HomeAction.BrowsePackages))); + entries.Add(new PaletteEntry("→ agents", "mediumpurple2","Agents", "orchestration agents", "agents orchestration", () => NavigateTo(HomeAction.BrowseAgents))); + entries.Add(new PaletteEntry("→ project", "deepskyblue1", "Project", "scan and install", "project scan recommend", () => NavigateTo(HomeAction.SyncProject))); + entries.Add(new PaletteEntry("→ analysis", "deepskyblue1", "Analysis", "catalog analysis", "analysis stats heaviest", () => NavigateTo(HomeAction.Analysis))); + entries.Add(new PaletteEntry("→ about", "grey", "About", "version and surface map", "about version", () => NavigateTo(HomeAction.About))); + + return entries; + } + + private sealed record PaletteEntry( + string IconLabel, + string AccentMarkup, + string Label, + string Detail, + string SearchHaystack, + Action Activate); + + private static ITheme BuildTheme() => new ModernGrayTheme + { + ListHoverBackgroundColor = SelectionBg, + ListHoverForegroundColor = SelectionFg, + ListUnfocusedHighlightBackgroundColor = UnfocusedSelectionBg, + ListUnfocusedHighlightForegroundColor = UnfocusedSelectionFg, + }; + + /// + /// A list control styled so the selected row is a solid inverted bar — the same bar whether + /// the row was reached by keyboard, mouse hover, or click (see ). + /// + private static ListBuilder StyledList(string? title = null) => Controls.List(title) + .WithScrollbarVisibility(ScrollbarVisibility.Auto) + .WithAutoHighlightOnFocus(true) + .WithHoverHighlighting(true) + .WithHighlightColors(SelectionFg, SelectionBg); + + // ------------------------------------------------------------------------- + // Interactive status bar (dynamic per page, clickable hints, highlighted keys) + // ------------------------------------------------------------------------- + + private void RebuildStatusBar(HomeAction? page) + { + var bar = _statusBar; + if (bar is null) + { + return; + } + + _currentPage = page; + bar.BatchUpdate(() => + { + bar.ClearAll(); + + bar.AddLeft("↑↓", "Move"); + bar.AddLeft("Enter", page is HomeAction.SyncProject ? "Install" : page is HomeAction.Workspace ? "Change" : "Open"); + if (IsListBearingPage(page)) + { + bar.AddLeft("/", "Search", ShowSearchOverlay); + } + bar.AddLeft("Ctrl+P", "Palette", () => { if (_ws is not null) ShowCommandPalette(_ws); }); + foreach (var (key, label, action) in PageShortcuts(page)) + { + bar.AddLeft(key, label, action); + } + bar.AddLeft("Ctrl+R", "Refresh", RefreshCatalogFromUi); + bar.AddLeft("Esc", "Quit", () => _ws?.Shutdown(0)); + + _statusMessage = bar.AddCenterText(string.Empty); + + bar.AddRightText($"[dim]v{Escape(skillCatalog.CatalogVersion)} · {skillCatalog.Skills.Count} skills[/]"); + bar.AddRightSeparator(); + _clockItem = bar.AddRightText(DateTime.Now.ToString("HH:mm:ss")); + }); + } + + private IEnumerable<(string Key, string Label, Action OnClick)> PageShortcuts(HomeAction? page) => page switch + { + HomeAction.ManageInstalled => new (string, string, Action)[] + { + ("Ctrl+U", "Update outdated", UpdateAllOutdatedFromUi), + ("Ctrl+Del", "Remove all", RemoveAllFromUi), + }, + HomeAction.SyncProject => new (string, string, Action)[] + { + ("Ctrl+I", "Install recommended", InstallAllRecommendedFromUi), + }, + _ => Array.Empty<(string, string, Action)>(), + }; + + private void RebuildActivePage() + { + if (_ws is null || _activePanel is null) + { + return; + } + + if (_currentPage is HomeAction action) + { + BuildActionPage(_ws, _activePanel, action); + } + else + { + BuildHomePage(_ws, _activePanel); + } + } + + /// + /// Shows a transient notification. Info/Success render only as a sliding card; Warning/Danger + /// also leave a sticky line in the bottom status bar until the next page change so the user + /// has time to read it. Default severity is Info. + /// + private void Toast(string message, NotificationSeverity? severity = null) + { + if (string.IsNullOrEmpty(message)) { ClearStickyStatus(); return; } + + var sev = severity ?? NotificationSeverity.Info; + _ws?.NotificationStateService.ShowNotification(title: string.Empty, message, sev); + + if (_statusMessage is not null) + { + if (sev == NotificationSeverity.Warning) + { + _statusMessage.Label = $"[yellow]⚠ {Escape(message)}[/]"; + } + else if (sev == NotificationSeverity.Danger) + { + _statusMessage.Label = $"[red]✘ {Escape(message)}[/]"; + } + else + { + // Info / Success / None — the slide-in card carries the feedback; keep the bar quiet. + _statusMessage.Label = string.Empty; + } + } + } + + private void ClearStickyStatus() + { + if (_statusMessage is not null) _statusMessage.Label = string.Empty; + } + + /// + /// Convenience for "install/remove" callers: a null result is treated as a failure (rendered + /// as a red toast with sticky status); a non-null result is success (transient green toast). + /// + private void ToastResult(object? result, string failureMessage, string successMessage) + { + if (result is null) + Toast(failureMessage, NotificationSeverity.Danger); + else + Toast(successMessage, NotificationSeverity.Success); + } + + /// + /// Case-insensitive substring test against the current search filter. Empty filter matches + /// everything. Tokens (any of the supplied parts) are matched independently — a row is kept + /// if ANY token contains the filter so callers can pass name + collection + lane as separate + /// tokens and get the expected "OR" behavior. + /// + private bool MatchesFilter(params string?[] tokens) + { + if (string.IsNullOrWhiteSpace(_searchFilter)) return true; + var needle = _searchFilter; + foreach (var token in tokens) + { + if (token is not null && token.Contains(needle, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + /// + /// Renders a small "filter: …" chip at the top of a list-bearing page so the user knows + /// the visible list is filtered. Caller is responsible for only emitting it when filter is set. + /// + private void AddSearchChip(ScrollablePanelControl panel) + { + if (string.IsNullOrWhiteSpace(_searchFilter)) return; + panel.AddControl(BuildNotePanel( + "filter", + $"[yellow]matching “{Escape(_searchFilter)}”[/] [grey50]· press[/] [bold]Esc[/] [grey50]to clear[/]", + AccentYellow)); + } + + private async Task ClockLoopAsync(Window window, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + + if (_clockItem is not null) + { + _clockItem.Label = DateTime.Now.ToString("HH:mm:ss"); + window.Invalidate(false); + } + } + } + + private void RefreshCatalogFromUi() + { + try + { + Toast("Refreshing catalog…", NotificationSeverity.Info); + LoadCatalogsAsync(refreshCatalog: true).GetAwaiter().GetResult(); + Toast($"Catalog refreshed: {skillCatalog.CatalogVersion} ({skillCatalog.Skills.Count} skills)", NotificationSeverity.Success); + } + catch (Exception exception) + { + Toast($"Refresh failed: {exception.Message}", NotificationSeverity.Danger); + } + + // RaiseSnapshotChanged fires the AttachSessionEvents handler which calls + // RebuildTopStatusBar() + RebuildActivePage(); also bump the bottom bar. + Session.RaiseSnapshotChanged(); + RebuildStatusBar(_currentPage); + } + + private void UpdateAllOutdatedFromUi() + { + var layout = ResolveSkillLayout(); + var outdated = SafeGet(() => new SkillInstaller(skillCatalog).GetInstalledSkills(layout), Array.Empty()) + .Where(record => !record.IsCurrent) + .ToArray(); + if (outdated.Length == 0) + { + Toast("No outdated skills in this target", NotificationSeverity.Warning); + return; + } + + var msg = UpdateSkillRecords(outdated); + Toast(msg, msg.Contains("failed", StringComparison.OrdinalIgnoreCase) ? NotificationSeverity.Danger : NotificationSeverity.Success); + RebuildActivePage(); + } + + private void RemoveAllFromUi() + { + if (_ws is null) + { + return; + } + + var layout = ResolveSkillLayout(); + var installed = SafeGet(() => new SkillInstaller(skillCatalog).GetInstalledSkills(layout), Array.Empty()); + if (installed.Count == 0) + { + Toast("Nothing to remove in this target", NotificationSeverity.Warning); + return; + } + + ConfirmModal(_ws, "Remove all installed skills?", $"Deletes every catalog skill from {layout.PrimaryRoot.FullName}.", () => + { + var summary = SafeGet(() => new SkillInstaller(skillCatalog).Remove(installed.Select(record => record.Skill).ToArray(), layout), default(SkillRemoveSummary)); + ToastResult(summary, "Remove failed", summary is null ? string.Empty : $"Removed {summary.RemovedCount} skill(s)"); + RebuildActivePage(); + }); + } + + private void InstallAllRecommendedFromUi() + { + var scan = SafeGet(() => new ProjectSkillRecommender(skillCatalog).Analyze(Session.ProjectDirectory), null); + if (scan is null) + { + Toast("Project scan failed", NotificationSeverity.Danger); + return; + } + + var layout = ResolveSkillLayout(); + var installedByName = SafeGet(() => new SkillInstaller(skillCatalog).GetInstalledSkills(layout), Array.Empty()) + .ToDictionary(record => record.Skill.Name, StringComparer.OrdinalIgnoreCase); + var newSkills = scan.Recommendations + .Where(r => !installedByName.ContainsKey(r.Skill.Name)) + .Select(r => r.Skill) + .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()) + .ToArray(); + var outdatedSkills = scan.Recommendations + .Where(r => installedByName.TryGetValue(r.Skill.Name, out var rec) && !rec.IsCurrent) + .Select(r => r.Skill) + .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()) + .ToArray(); + if (newSkills.Length == 0 && outdatedSkills.Length == 0) + { + Toast("No new recommended skills to install", NotificationSeverity.Warning); + return; + } + + // force=true on outdated entries so existing skill dirs are overwritten, force=false on new ones. + var installer = new SkillInstaller(skillCatalog); + var newSummary = newSkills.Length == 0 ? default : SafeGet(() => installer.Install(newSkills, layout, force: false), default(SkillInstallSummary)); + var updateSummary = outdatedSkills.Length == 0 ? default : SafeGet(() => installer.Install(outdatedSkills, layout, force: true), default(SkillInstallSummary)); + var installedCount = (newSummary?.InstalledCount ?? 0) + (updateSummary?.InstalledCount ?? 0); + var skippedCount = (newSummary?.SkippedExisting.Count ?? 0) + (updateSummary?.SkippedExisting.Count ?? 0); + var failed = installedCount == 0 && skippedCount == 0; + Toast(failed ? "Install failed" : $"Installed {installedCount}, skipped {skippedCount}", failed ? NotificationSeverity.Danger : NotificationSeverity.Success); + RebuildActivePage(); + } + + private static int SafeCount(Func getter) + { + try + { + return getter(); + } + catch + { + return 0; + } + } + + private static T SafeGet(Func getter, T fallback) + { + try + { + return getter(); + } + catch + { + return fallback; + } + } + + private static int SafeConsole(Func getter, int fallback) + { + try + { + var value = getter(); + return value > 0 ? value : fallback; + } + catch + { + return fallback; + } + } +} diff --git a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs index 2bcfca1..2f0091f 100644 --- a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs +++ b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs @@ -4,7 +4,7 @@ namespace ManagedCode.DotnetSkills; -internal sealed class InteractiveConsoleApp +internal sealed partial class InteractiveConsoleApp { private readonly IInteractivePrompts prompts; private readonly Func> loadSkillCatalogAsync; @@ -46,7 +46,12 @@ public InteractiveConsoleApp( internal InteractiveSessionState Session { get; } - public async Task RunAsync() + /// + /// Legacy prompt-first interactive shell (Spectre based). + /// Retained as a fallback for non-interactive terminals; the default surface is the + /// SharpConsoleUI command center (see InteractiveConsoleApp.Shell.cs). + /// + public async Task RunClassicShellAsync() { toolUpdateStatus = await getToolUpdateStatusAsync(cachePath); await LoadCatalogsAsync(refreshCatalog: false); @@ -3713,13 +3718,74 @@ internal static string BuildHomeActionMenuLabel(HomeActionView action) internal sealed class InteractiveSessionState { - public AgentPlatform Agent { get; set; } + private AgentPlatform _agent; + private InstallScope _scope; + private string? _projectDirectory; + private bool _suspend; - public InstallScope Scope { get; set; } + // Raised after the corresponding property changes to a new value. The shell + // subscribes from BuildActionPage/BuildHomePage to refresh the open page when + // session identity flips from anywhere (Settings, command palette, etc.). + public event Action? AgentChanged; + public event Action? ScopeChanged; + public event Action? ProjectChanged; + public event Action? SnapshotChanged; - public string? ProjectDirectory { get; set; } + public AgentPlatform Agent + { + get => _agent; + set + { + if (EqualityComparer.Default.Equals(_agent, value)) return; + _agent = value; + if (!_suspend) AgentChanged?.Invoke(); + } + } + + public InstallScope Scope + { + get => _scope; + set + { + if (EqualityComparer.Default.Equals(_scope, value)) return; + _scope = value; + if (!_suspend) ScopeChanged?.Invoke(); + } + } + + public string? ProjectDirectory + { + get => _projectDirectory; + set + { + if (string.Equals(_projectDirectory, value, StringComparison.Ordinal)) return; + _projectDirectory = value; + if (!_suspend) ProjectChanged?.Invoke(); + } + } public bool BundledOnly { get; set; } + + /// + /// Suspends Agent/Scope/Project events while runs, then fires + /// SnapshotChanged once at the end so callers get a single refresh signal. + /// + public void RunSuspended(Action action) + { + _suspend = true; + try { action(); } + finally + { + _suspend = false; + SnapshotChanged?.Invoke(); + } + } + + /// + /// Fires SnapshotChanged — used after operations (like catalog refresh) that change + /// session-relevant state without going through the individual setters. + /// + public void RaiseSnapshotChanged() => SnapshotChanged?.Invoke(); } internal sealed record MenuOption(string Label, T Value); diff --git a/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj b/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj index c79749c..b9cfa1f 100644 --- a/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj +++ b/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj @@ -46,6 +46,7 @@ + diff --git a/cli/ManagedCode.DotnetSkills/Runtime/CatalogOrganization.cs b/cli/ManagedCode.DotnetSkills/Runtime/CatalogOrganization.cs index cd55617..0f1300d 100644 --- a/cli/ManagedCode.DotnetSkills/Runtime/CatalogOrganization.cs +++ b/cli/ManagedCode.DotnetSkills/Runtime/CatalogOrganization.cs @@ -130,6 +130,23 @@ internal static class CatalogOrganization "Stryker", }; + private static readonly HashSet TestingResearchSkillNames = new(StringComparer.OrdinalIgnoreCase) + { + "assertion-quality", + "code-testing-agent", + "exp-assertion-quality", + "exp-mock-usage-analysis", + "exp-test-gap-analysis", + "exp-test-maintainability", + "exp-test-smell-detection", + "exp-test-tagging", + "mock-usage-analysis", + "test-gap-analysis", + "test-maintainability", + "test-smell-detection", + "test-tagging", + }; + private static readonly HashSet LegacyPackages = new(StringComparer.OrdinalIgnoreCase) { "Entity-Framework-6", @@ -372,7 +389,7 @@ private static bool IsTestingSkill(string type, string category) private static bool IsTestingResearchSkill(string package, string category, string name) { - return string.Equals(name, "code-testing-agent", StringComparison.OrdinalIgnoreCase) + return TestingResearchSkillNames.Contains(name) || string.Equals(package, "Stryker", StringComparison.OrdinalIgnoreCase) || string.Equals(package, "Official-DotNet-Experimental", StringComparison.OrdinalIgnoreCase) && string.Equals(category, "Testing", StringComparison.OrdinalIgnoreCase); diff --git a/external-sources/upstreams/dotnet-skills/dotnet-aspnet/skills/dotnet-webapi/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-aspnet/skills/dotnet-webapi/SKILL.md new file mode 100644 index 0000000..c22ba5c --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-aspnet/skills/dotnet-webapi/SKILL.md @@ -0,0 +1,506 @@ +--- +name: dotnet-webapi +description: > + Guides creation and modification of ASP.NET Core Web API endpoints with + correct HTTP semantics, OpenAPI metadata, and error handling. + USE FOR: adding new API endpoints (controllers or minimal APIs), wiring up + OpenAPI/Swagger, creating .http test files, setting up global error handling + middleware. + DO NOT USE FOR: general C# coding style, EF Core data access or query + optimization (use optimizing-ef-core-queries), frontend/Blazor work, gRPC + services, or SignalR hubs. +license: MIT +--- + +# ASP.NET Core Web API + +Produce well-structured ASP.NET Core Web API endpoints with proper HTTP +semantics, OpenAPI documentation, and error handling. + +## When to Use + +Use this skill when working on ASP.NET Core HTTP APIs, including: + +- adding or modifying Web API endpoints implemented with controllers or minimal APIs; +- wiring up OpenAPI/Swagger metadata and endpoint documentation; +- defining request/response DTOs and consistent HTTP status code behavior; +- adding `.http` files or similar request-based API testing artifacts; +- configuring centralized API error handling middleware or exception mapping. + +## When Not to Use + +Do not use this skill for: + +- general C# coding style or non-API refactoring; +- EF Core data modeling or query optimization work; use `optimizing-ef-core-queries`; +- frontend, Razor, or Blazor UI changes; +- gRPC services; +- SignalR hubs or real-time messaging flows. + +## Inputs / prerequisites + +Before applying this skill, gather the project context needed to match the +existing API style and wiring: + +- the ASP.NET Core entry point, typically `Program.cs`; +- any existing controllers, especially classes inheriting `ControllerBase` or + using `[ApiController]`; +- any existing minimal API registrations such as `app.MapGet`, `app.MapPost`, + `app.MapPut`, or `app.MapDelete`; +- related DTO, model, validation, and error-handling types already used by the project; +- available build, run, and test commands so changes can be verified. + +If the user asks for a new endpoint, inspect the current project structure first +so the implementation follows the established conventions rather than mixing styles. +## Workflow + +### Step 1: Determine the API style + +Scan the project for existing endpoint patterns before writing any code. + +1. Search for classes inheriting `ControllerBase` or decorated with `[ApiController]`. +2. Search `Program.cs` or endpoint files for `app.MapGet`, `app.MapPost`, etc. +3. If the project already uses **controllers**, continue with controllers. +4. If the project already uses **minimal APIs**, continue with minimal APIs. +5. If neither exists (new project), **default to minimal APIs** unless the user + explicitly requests controllers. + +Do not mix styles in the same project. + +### Step 2: Define request and response types + +Create dedicated types for API input and output. Never expose EF Core entities +directly in request or response bodies. + +**Use `sealed record` for all DTOs.** Records enforce immutability, provide +value-based equality, and produce concise code. Seal them to prevent unintended +inheritance and enable JIT devirtualization (CA1852). + +**Naming convention:** + +| Role | Convention | Example | +|------|-----------|---------| +| Input (create) | `Create{Entity}Request` | `CreateProductRequest` | +| Input (update) | `Update{Entity}Request` | `UpdateProductRequest` | +| Output (single) | `{Entity}Response` | `ProductResponse` | +| Output (list) | `{Entity}ListResponse` | `ProductListResponse` | + +**XML doc comments on all DTOs:** Add `` XML doc comments to every +request and response type exposed in the API. These comments are automatically +included in the generated OpenAPI specification, producing richer documentation +without extra metadata calls. + +Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments + +**Date and time values — use `DateTimeOffset`:** When a DTO includes a date or +time property, always use `DateTimeOffset` instead of `DateTime`. +`DateTimeOffset` preserves the UTC offset, avoids ambiguous timezone +conversions, and serializes to ISO 8601 with offset information in JSON — which +is what API consumers expect. + +Reference: https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset +**JSON serialization options — preserve existing behavior by default:** For +existing APIs, do **not** introduce stricter serialization/deserialization settings +unless the project already uses them or the user explicitly asks for them. Settings +such as case-sensitive property matching and strict number handling can break +existing clients. For **new projects**, or when strict JSON handling is explicitly +requested, configure options like the following to minimize the potential of +processing malicious requests: + +```csharp +// Apply these settings only for new projects, when the existing project already +// uses them, or when the user explicitly requests stricter JSON behavior. +builder.Services.ConfigureHttpJsonOptions(options => +{ + // disallow reading numbers from JSON strings + options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; + // match properties with exact casing during deserialization + options.SerializerOptions.PropertyNameCaseInsensitive = false; + // reject duplicate JSON property names during deserialization + options.SerializerOptions.AllowDuplicateProperties = false; + // omit null properties from serialized output + options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); +``` +**Enum properties — serialize as strings by default:** Unless the user +explicitly requests integer serialization, all enum properties should be +serialized as strings. String-serialized enums are human-readable, less fragile +when values are reordered, and produce better OpenAPI documentation. See Step 4 +for the `JsonStringEnumConverter` configuration. + +**Response DTOs** — use positional sealed records for concise, immutable output: + +```csharp +/// Represents a product returned by the API. +public sealed record ProductResponse( + int Id, + string Name, + decimal Price, + Category Category, + bool IsAvailable, + DateTimeOffset CreatedAt); +``` + +**Request DTOs** — use sealed records with `init` properties so data annotations +work naturally: + +```csharp +/// Payload for creating a new product. +public sealed record CreateProductRequest +{ + [Required, MaxLength(200)] + public required string Name { get; init; } + + [Range(0.01, 999999.99)] + public required decimal Price { get; init; } + + public required Category Category { get; init; } +} +``` + +Follow the same pattern for `Update{Entity}Request` records, adding any +additional properties the update requires (e.g., `IsAvailable`). + +**Minimal API validation — register explicitly:** Data-annotation validation +(`[Required]`, `[MaxLength]`, `[Range]`, etc.) is automatic in MVC controllers, +but minimal APIs require explicit opt-in. For **.NET 10+** projects using minimal +APIs, add the validation services in `Program.cs`: + +```csharp +builder.Services.AddValidation(); +``` + +This wires up an endpoint filter that validates parameters decorated with data +annotations before the handler executes, returning a `400 Bad Request` with a +validation problem details response on failure. + +Reference: https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0 + +**Do not** use mutable classes (`{ get; set; }`) for DTOs. Mutable DTOs allow +accidental modification after construction and lose the self-documenting +immutability that records provide. + +### Step 3: Implement the endpoints + +Whether using controllers or minimal APIs, follow these HTTP conventions +consistently. + +**Organizing minimal API endpoints:** For projects using minimal APIs, organize +endpoints by resource using static classes with a static `Map` method. +This pattern keeps endpoint definitions grouped by resource type, making the +code more maintainable and easier to navigate as the API grows. + +**Pattern structure:** + +1. Create one static class per resource (e.g., `ProductEndpoints`, `CategoryEndpoints`). +2. Define a static `Map(this WebApplication app)` extension method. +3. Inside the method, call `MapGet`, `MapPost`, `MapPut`, `MapDelete`, etc. for + that resource's endpoints. +4. In `Program.cs`, call each resource's `Map` method in order. + +**Minimal API return types — prefer `TypedResults`:** + +Always prefer `TypedResults` over the `Results` factory. `TypedResults` embeds +response type information in the method signature, giving the OpenAPI generator +richer metadata automatically. + +When a handler returns **multiple result types** (e.g., `Ok` or `NotFound`), +annotate the lambda with an explicit `Results` return type. This +lets you use `TypedResults` while still giving the compiler a common type: + +```csharp +async Task, NotFound>> (int id, ...) => ... +``` + +**Do not** use `TypedResults.Ok(x)` and `TypedResults.NotFound()` in a bare +ternary without an explicit return type annotation. `Ok` and `NotFound` are +different types with no common base the compiler can infer, which causes +`CS1593: Delegate 'RequestDelegate' does not take N arguments` because the +compiler falls back to matching `RequestDelegate(HttpContext)`. + +**Fallback — `Results` factory:** If a handler has many conditional branches +(7+ result types), you may use the `Results` factory (`Results.Ok()`, +`Results.NotFound()`) which returns `IResult`, sacrificing compile-time OpenAPI +inference for simpler signatures. + +**Status codes:** + +| Operation | Success | Common errors | +|-----------|---------|---------------| +| GET (single) | `200 OK` | `404 Not Found` | +| GET (list) | `200 OK` | — | +| POST (create) | `201 Created` with `Location` header | `400 Bad Request`, `409 Conflict` | +| PUT (full update) | `200 OK` | `400 Bad Request`, `404 Not Found` | +| PATCH (partial/action) | `200 OK` | `400 Bad Request`, `404 Not Found` | +| DELETE | `204 No Content` | `404 Not Found`, `409 Conflict` | + +**POST 201 responses:** Always return a `Location` header pointing to the +newly created resource. + +- Controllers: use `CreatedAtAction(nameof(GetById), new { id = ... }, response)` +- Minimal APIs: use `TypedResults.Created($"/api/products/{id}", response)` + +**CancellationToken:** Accept `CancellationToken` in every endpoint signature +and forward it through to all async calls (service methods, EF Core queries, +`HttpClient` calls). This allows the server to stop work when a client +disconnects. + +```csharp +// Controller example +[HttpGet("{id}")] +public async Task> GetById( + int id, CancellationToken cancellationToken) +{ + var product = await _productService.GetByIdAsync(id, cancellationToken); + return product is null ? NotFound() : Ok(product); +} + +// Minimal API example — TypedResults with explicit return type (recommended) +app.MapGet("/api/products/{id}", async Task, NotFound>> ( + int id, IProductService service, CancellationToken cancellationToken) => +{ + var product = await service.GetByIdAsync(id, cancellationToken); + return product is null ? TypedResults.NotFound() : TypedResults.Ok(product); +}); +``` + +### Step 4: Wire up OpenAPI + +Every ASP.NET Core Web API should have OpenAPI documentation. Check whether +the project already has OpenAPI configured before adding it. + +**For .NET 9+ projects**, use the built-in ASP.NET Core OpenAPI support +(`builder.Services.AddOpenApi()` + `app.MapOpenApi()` in development). +This is all that is needed — no additional packages required. + +**Do NOT add any `Swashbuckle.*` NuGet package** (`Swashbuckle.AspNetCore`, +`Swashbuckle.AspNetCore.SwaggerUI`, `Swashbuckle.AspNetCore.SwaggerGen`, +etc.) to .NET 9+ projects. Swashbuckle has known compatibility issues with +.NET 9+ and .NET 10 OpenAPI types. For projects targeting .NET 8 or earlier, +Swashbuckle is acceptable. If the project already has Swashbuckle installed, +keep it unless the user asks to remove it. + +Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview + +**OpenAPI metadata on endpoints:** Add descriptive metadata so the generated +documentation is useful, not just a list of routes. For minimal APIs, chain +the metadata methods: + +```csharp +app.MapGet("/api/products/{id}", handler) + .WithName("GetProductById") + .WithSummary("Get a product by ID") + .WithDescription("Returns the full product details including category.") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); +``` + +**Enum serialization (strings by default):** Configure JSON serialization so +enums appear as readable strings in both API responses and OpenAPI schemas. +Always add this configuration unless the user explicitly requests integer +enum serialization. Configure it for both minimal APIs and controllers, as +they use different option types: + +```csharp +// Minimal APIs +builder.Services.ConfigureHttpJsonOptions(options => + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); + +// Controllers / MVC +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); +``` + +### Step 5: Set up error handling + +Use a global exception handler so that individual endpoints do not need +try-catch blocks. Return RFC 7807 Problem Details for all error responses. + +**For .NET 8+ projects**, prefer the built-in exception handler middleware: + +```csharp +builder.Services.AddProblemDetails(); + +app.UseExceptionHandler(); +app.UseStatusCodePages(); +``` + +If the project needs custom exception-to-status-code mapping (e.g., a +`NotFoundException` should return 404), implement `IExceptionHandler`: + +```csharp +internal sealed class ApiExceptionHandler(ILogger logger) + : IExceptionHandler +{ + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + var (statusCode, title) = exception switch + { + KeyNotFoundException => (StatusCodes.Status404NotFound, "Not Found"), + ArgumentException => (StatusCodes.Status400BadRequest, "Bad Request"), + InvalidOperationException => (StatusCodes.Status409Conflict, "Conflict"), + _ => (0, (string?)null) + }; + + if (statusCode == 0) + return false; // Let the default handler deal with it + + // Important: returning true below suppresses the exception diagnostics middleware + // for this exception, so ensure it is logged/telemetrized before returning. + logger.LogWarning(exception, "Handled API exception: {Title}", title); + + httpContext.Response.StatusCode = statusCode; + await httpContext.Response.WriteAsJsonAsync(new ProblemDetails + { + Status = statusCode, + Title = title, + // Do not use exception.Message here — it may leak sensitive internal details. + // Use a safe, user-facing message instead. + Detail = title, + Instance = httpContext.Request.Path + }, cancellationToken); + + return true; + } +} +``` + +Register it: + +```csharp +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +app.UseExceptionHandler(); +``` + +**File placement:** Always place exception handler classes in a `Middleware/` +folder to maintain consistent project organization. Do not place them at the +project root. + +### Step 6: Use a service layer + +Do not inject data stores directly into controllers or endpoint handlers. +Create a service interface and a sealed implementation class that owns the +data access logic and mapping between entities and request/response types. + +Always define an interface for every service — this enables unit testing with +mocks and follows the Dependency Inversion Principle: + +```csharp +// Services/IProductService.cs +public interface IProductService +{ + Task> GetAllAsync(CancellationToken ct); + Task GetByIdAsync(int id, CancellationToken ct); + Task CreateAsync(CreateProductRequest request, CancellationToken ct); +} + +// Services/ProductService.cs +public sealed class ProductService(...) : IProductService +{ + // Data access logic, entity-to-DTO mapping +} +``` + +Register with the interface, not the concrete type: + +```csharp +// In Program.cs +builder.Services.AddScoped(); +``` + +For EF Core data access patterns (migrations, Fluent API configuration, +`AsNoTracking`, seed data), see the `optimizing-ef-core-queries` skill. + +### Step 7: Create a .http test file + +After implementing endpoints, create a `.http` file in the project root that +demonstrates how to call every new endpoint. This serves as living +documentation and a quick manual test harness. + +```http +@baseUrl = http://localhost:5000 + +### Get all products +GET {{baseUrl}}/api/products + +### Get product by ID +GET {{baseUrl}}/api/products/1 + +### Create a product +POST {{baseUrl}}/api/products +Content-Type: application/json + +{ + "name": "Wireless Mouse", + "price": 29.99, + "category": "Electronics" +} + +### Delete a product +DELETE {{baseUrl}}/api/products/1 +``` + +Include at least one request per endpoint with realistic bodies. Show error +paths (e.g., non-existent IDs). Match the port to `launchSettings.json`. + +### Step 8: Build and verify + +1. Run `dotnet build` — confirm zero errors and zero warnings. +2. Start the app and verify the OpenAPI document loads (default: `/openapi/v1.json`). +3. Run the requests in the `.http` file and confirm correct status codes. + +## Validation + +- [ ] All endpoints return correct HTTP status codes per the table in Step 3 +- [ ] POST endpoints return `201 Created` with a `Location` header +- [ ] DELETE endpoints return `204 No Content` +- [ ] Every endpoint signature includes `CancellationToken` +- [ ] `CancellationToken` is forwarded to all downstream async calls +- [ ] OpenAPI document is generated and includes all new endpoints +- [ ] Endpoints have summary/description metadata for OpenAPI +- [ ] Enum values appear as strings in JSON responses and OpenAPI schemas (unless user explicitly requested integer serialization) +- [ ] Error responses use RFC 7807 Problem Details format +- [ ] Domain entities are not exposed directly in API request/response bodies +- [ ] All API-exposed DTOs have `` XML doc comments +- [ ] Date and time properties use `DateTimeOffset`, not `DateTime` +- [ ] A `.http` file exists with a request for every new endpoint +- [ ] `dotnet build` passes with zero errors and zero warnings +- [ ] All DTOs are `sealed record` types (not mutable classes) +- [ ] Minimal API handlers use `TypedResults` with explicit `Results` return types +- [ ] Every service has a corresponding interface registered in DI +- [ ] Exception handlers are placed in the `Middleware/` folder + +## Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Exposing domain entities as API responses | Create separate `sealed record` request/response types. Entities leak navigation properties and internal fields. | +| Forgetting `CancellationToken` | Add to every endpoint and forward through the entire async call chain. | +| Returning `200 OK` from POST create | Return `201 Created` with a `Location` header. | +| Missing OpenAPI metadata | Chain `.WithName()`, `.WithSummary()`, `.WithDescription()`, `.Produces()` on every endpoint. | +| Injecting data stores directly into endpoints | Use a service layer with an interface for separation and testability. | +| Mixing controller and minimal API styles | Pick one per project and be consistent. | +| `TypedResults` in ternary without explicit return type | `Ok` and `NotFound` have no common base — annotate with `Task, NotFound>>` or fall back to `Results` factory. | +| Using mutable classes for DTOs | Use `sealed record` with positional syntax (responses) or `init` properties (requests). | +| Registering services without interfaces | Define `IService` and register with `AddScoped()`. | +| Adding any `Swashbuckle.*` package to new .NET 9+ projects | Use built-in `AddOpenApi()` + `MapOpenApi()`. Do not add `Swashbuckle.AspNetCore`, `Swashbuckle.AspNetCore.SwaggerUI`, or any other Swashbuckle package. | +| Missing XML doc comments on DTOs | Add `` XML doc comments to every request and response type. These flow into the generated OpenAPI spec automatically. | +| Using `DateTime` for date/time properties | Use `DateTimeOffset` instead — it preserves UTC offset, avoids timezone ambiguity, and serializes correctly in JSON. | +| Serializing enums as integers | Configure `JsonStringEnumConverter` so enums serialize as strings by default. Only use integer serialization if the user explicitly requests it. | + +## More Info + +- [ASP.NET Core Web API overview](https://learn.microsoft.com/en-us/aspnet/core/web-api/) — fundamental concepts for building Web APIs +- [OpenAPI in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview) — built-in OpenAPI support in .NET 9+ +- [OpenAPI from XML comments](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/openapi-comments) — how XML doc comments flow into the OpenAPI spec +- [Minimal APIs overview](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) — routing, parameter binding, and response types +- [Handle errors in ASP.NET Core APIs](https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors) — Problem Details and exception handling +- [DateTimeOffset](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset) — preferred type for date/time values in APIs diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-dotnet-test-frameworks/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-dotnet-test-frameworks/SKILL.md deleted file mode 100644 index fe4b2dd..0000000 --- a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-dotnet-test-frameworks/SKILL.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -name: exp-dotnet-test-frameworks -description: "Reference data for .NET test framework detection patterns, assertion APIs, skip annotations, setup/teardown methods, and common test smell indicators across MSTest, xUnit, NUnit, and TUnit. Loaded by test analysis skills (exp-test-smell-detection, exp-assertion-quality, exp-test-maintainability, exp-test-tagging) as framework-specific lookup tables." -user-invocable: false -license: MIT ---- - -# .NET Test Framework Reference - -Language-specific detection patterns for .NET test frameworks (MSTest, xUnit, NUnit, TUnit). - -## Test File Identification - -| Framework | Test class markers | Test method markers | -| --------- | ------------------ | ------------------- | -| MSTest | `[TestClass]` | `[TestMethod]`, `[DataTestMethod]` | -| xUnit | _(none — convention-based)_ | `[Fact]`, `[Theory]` | -| NUnit | `[TestFixture]` | `[Test]`, `[TestCase]`, `[TestCaseSource]` | -| TUnit | `[ClassDataSource]` | `[Test]` | - -## Assertion APIs by Framework - -| Category | MSTest | xUnit | NUnit | -| -------- | ------ | ----- | ----- | -| Equality | `Assert.AreEqual` | `Assert.Equal` | `Assert.That(x, Is.EqualTo(y))` | -| Boolean | `Assert.IsTrue` / `Assert.IsFalse` | `Assert.True` / `Assert.False` | `Assert.That(x, Is.True)` | -| Null | `Assert.IsNull` / `Assert.IsNotNull` | `Assert.Null` / `Assert.NotNull` | `Assert.That(x, Is.Null)` | -| Exception | `Assert.Throws()` / `Assert.ThrowsExactly()` | `Assert.Throws()` | `Assert.That(() => ..., Throws.TypeOf())` | -| Collection | `CollectionAssert.Contains` | `Assert.Contains` | `Assert.That(col, Has.Member(x))` | -| String | `StringAssert.Contains` | `Assert.Contains(str, sub)` | `Assert.That(str, Does.Contain(sub))` | -| Type | `Assert.IsInstanceOfType` | `Assert.IsAssignableFrom` | `Assert.That(x, Is.InstanceOf())` | -| Inconclusive | `Assert.Inconclusive()` | _skip via `[Fact(Skip)]`_ | `Assert.Inconclusive()` | -| Fail | `Assert.Fail()` | `Assert.Fail()` (.NET 10+) | `Assert.Fail()` | - -Third-party assertion libraries: `Should*` (Shouldly), `.Should()` (FluentAssertions / AwesomeAssertions), `Verify()` (Verify). - -## Sleep/Delay Patterns - -| Pattern | Example | -| ------- | ------- | -| Thread sleep | `Thread.Sleep(2000)` | -| Task delay | `await Task.Delay(1000)` | -| SpinWait | `SpinWait.SpinUntil(() => condition, timeout)` | - -## Skip/Ignore Annotations - -| Framework | Annotation | With reason | -| --------- | ---------- | ----------- | -| MSTest | `[Ignore]` | `[Ignore("reason")]` | -| xUnit | `[Fact(Skip = "reason")]` | _(reason is required)_ | -| NUnit | `[Ignore("reason")]` | _(reason is required)_ | -| TUnit | `[Skip("reason")]` | _(reason is required)_ | -| Conditional | `#if false` / `#if NEVER` | _(no reason possible)_ | - -## Exception Handling — Idiomatic Alternatives - -When a test uses `try`/`catch` to verify exceptions, suggest the framework-native alternative: - -**MSTest:** - -```csharp -// Instead of try/catch (matches exact type): -var ex = Assert.ThrowsExactly( - () => processor.ProcessOrder(emptyOrder)); -Assert.AreEqual("Order must contain at least one item", ex.Message); - -// Or (also matches derived types): -var ex = Assert.Throws( - () => processor.ProcessOrder(emptyOrder)); -Assert.AreEqual("Order must contain at least one item", ex.Message); -``` - -**xUnit:** - -```csharp -var ex = Assert.Throws( - () => processor.ProcessOrder(emptyOrder)); -Assert.Equal("Order must contain at least one item", ex.Message); -``` - -**NUnit:** - -```csharp -var ex = Assert.Throws( - () => processor.ProcessOrder(emptyOrder)); -Assert.That(ex.Message, Is.EqualTo("Order must contain at least one item")); -``` - -## Mystery Guest — Common .NET Patterns - -| Smell indicator | What to look for | -| --------------- | ---------------- | -| File system | `File.ReadAllText`, `File.Exists`, `File.WriteAllBytes`, `Directory.GetFiles`, `Path.Combine` with hard-coded paths | -| Database | `SqlConnection`, `DbContext` (without in-memory provider), `SqlCommand` | -| Network | `HttpClient` without `HttpMessageHandler` override, `WebRequest`, `TcpClient` | -| Environment | `Environment.GetEnvironmentVariable`, `Environment.CurrentDirectory` | -| Acceptable | `MemoryStream`, `StringReader`, `InMemory` database providers, custom `DelegatingHandler` | - -## Integration Test Markers - -Recognize these as integration tests (adjust smell severity accordingly): - -- Class name contains `Integration`, `E2E`, `EndToEnd`, or `Acceptance` -- `[TestCategory("Integration")]` (MSTest) -- `[Trait("Category", "Integration")]` (xUnit) -- `[Category("Integration")]` (NUnit) -- Project name ending in `.IntegrationTests` or `.E2ETests` - -## Setup/Teardown Methods - -| Framework | Setup | Teardown | -| --------- | ----- | -------- | -| MSTest | `[TestInitialize]` or constructor | `[TestCleanup]` or `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` | -| xUnit | constructor | `IDisposable.Dispose` / `IAsyncDisposable.DisposeAsync` | -| NUnit | `[SetUp]` | `[TearDown]` | -| MSTest (class) | `[ClassInitialize]` | `[ClassCleanup]` | -| NUnit (class) | `[OneTimeSetUp]` | `[OneTimeTearDown]` | -| xUnit (class) | `IClassFixture` | fixture's `Dispose` | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-maintainability/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-maintainability/SKILL.md index 4a6352e..64d780b 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-maintainability/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-maintainability/SKILL.md @@ -36,7 +36,7 @@ Analyze .NET test code for maintainability issues: duplicated boilerplate, copy- ### Step 1: Gather the test code -Read all test files the user provides or references. If the user points to a directory or project, scan for all test files — see the `exp-dotnet-test-frameworks` skill for framework-specific markers. +Read all test files the user provides or references. If the user points to a directory or project, scan for all test files — see the `dotnet-test-frameworks` skill for framework-specific markers. ### Step 2: Identify maintainability issues diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/agents/build-perf.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/agents/build-perf.agent.md index 611e169..ae0ea50 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/agents/build-perf.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/agents/build-perf.agent.md @@ -18,16 +18,29 @@ Before starting any analysis, verify the context is MSBuild-related. If the work ### Step 1: Establish Baseline - Run the build with binlog: `dotnet build /bl:perf-baseline.binlog -m` -- Replay to diagnostic log: `dotnet msbuild perf-baseline.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` -- Record total build duration (from build output) and node count +- Record total build duration from build output -### Step 2: Top-down Analysis -Analyze the replayed diagnostic log: -1. `grep 'Target Performance Summary' -A 50 full.log` → find dominant targets and their cumulative time -2. `grep 'Task Performance Summary' -A 50 full.log` → find dominant tasks -3. `grep 'Project Performance Summary' -A 50 full.log` → find time-heavy projects -4. `grep -i 'Total analyzer execution time\|analyzer.*elapsed' full.log` → check analyzer overhead -5. `grep -i 'node.*assigned\|Building with' full.log | head -30` → assess parallelism +### Step 2: Top-down Analysis — binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) which is bundled with this plugin. Call `tools/list` for the MCP first if you are unsure which tools are available. + +1. Use overview tool → understand build status and duration +2. Use expensive_projects tool → find the slowest projects +3. Use expensive_targets tool → find dominant targets and their cumulative time +4. Use expensive_tasks tool → find dominant tasks +5. Use expensive_analyzers tool → check analyzer overhead +6. Drill into specific projects with project_target_times tool + +**Important:** The `.binlog` file is a binary format — do NOT try to `cat`, `head`, `strings`, or read it directly. Use only the MCP tools to query it. + +### Alternate flow — text-log replay (when MCP is unavailable) + +1. Replay to diagnostic log: `dotnet msbuild perf-baseline.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` +2. `grep 'Target Performance Summary' -A 50 full.log` → find dominant targets and their cumulative time +3. `grep 'Task Performance Summary' -A 50 full.log` → find dominant tasks +4. `grep 'Project Performance Summary' -A 50 full.log` → find time-heavy projects +5. `grep -i 'Total analyzer execution time\|analyzer.*elapsed' full.log` → check analyzer overhead +6. `grep -i 'node.*assigned\|Building with' full.log | head -30` → assess parallelism ### Step 3: Bottleneck Classification Classify findings into categories: @@ -39,7 +52,9 @@ Classify findings into categories: - **Analyzers**: disproportionate analyzer time → specific analyzer is expensive ### Step 4: Deep Dive -For each identified bottleneck: +For each identified bottleneck, use MCP tools (task_details, search, properties, items) to drill into specifics. + +When MCP is unavailable, fall back to text-log grep: - `grep 'Target "TargetName"' full.log` → find specific target execution across projects - `grep -i 'Csc.*elapsed\|Csc.*duration' full.log` → check compilation times - `grep 'specific pattern' full.log` → search for specific issues diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/plugin.json b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/plugin.json index 96b13a3..9fdf735 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/plugin.json +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/plugin.json @@ -7,5 +7,20 @@ "./agents/build-perf.agent.md", "./agents/msbuild-code-review.agent.md", "./agents/msbuild.agent.md" - ] + ], + "mcpServers": { + "binlog": { + "type": "stdio", + "command": "dotnet", + "args": [ + "dnx", + "Microsoft.AITools.BinlogMcp", + "--yes", + "--prerelease", + "--add-source", + "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" + ], + "tools": ["*"] + } + } } diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/binlog-failure-analysis/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/binlog-failure-analysis/SKILL.md index 7a517e7..f1419cc 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/binlog-failure-analysis/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/binlog-failure-analysis/SKILL.md @@ -1,18 +1,41 @@ --- name: binlog-failure-analysis -description: "Analyze MSBuild binary logs to diagnose build failures by replaying binlogs to searchable text logs. Only activate in MSBuild/.NET build context. USE FOR: build errors that are unclear from console output, diagnosing cascading failures across multi-project builds, tracing MSBuild target execution order, investigating common errors like CS0246 (type not found), MSB4019 (imported project not found), NU1605 (package downgrade), MSB3277 (version conflicts), and ResolveProjectReferences failures. Requires an existing .binlog file. DO NOT USE FOR: generating binlogs (use binlog-generation), build performance analysis (use build-perf-diagnostics), non-MSBuild build systems. INVOKES: dotnet msbuild binlog replay, grep, cat, head, tail for log analysis." +description: "Analyze MSBuild binary logs to diagnose build failures. Only activate in MSBuild/.NET build context. USE FOR: build errors that are unclear from console output, diagnosing cascading failures across multi-project builds, tracing MSBuild target execution order, and generally any MSBuild build issues. Requires an existing .binlog file. DO NOT USE FOR: generating binlogs (use binlog-generation), non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, errors, search, items, properties); falls back to dotnet msbuild binlog replay + grep/cat when the MCP is unavailable." license: MIT --- # Analyzing MSBuild Failures with Binary Logs -Use MSBuild's built-in **binlog replay** to convert binary logs into searchable text logs, then analyze with standard tools (`grep`, `cat`, `head`, `tail`, `find`). +This skill diagnoses MSBuild build failures from a `.binlog` file. The preferred +path uses the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the +`binlog` MCP namespace) which is bundled with this plugin. If the MCP server is +not available, fall back to the **binlog replay** workflow at the bottom. -## Build Error Investigation (Primary Workflow) +## Primary workflow — binlog MCP -### Step 1: Replay the binlog to text logs +The MCP server exposes structured tools for inspecting a `.binlog` without +parsing text logs. Call them directly instead of replaying the binlog to a text +file. Call `tools/list` for the MCP first if you are unsure which tools are available. -Replay produces multiple focused log files in one pass: +**Important constraints:** +- The `.binlog` file is a **binary format** — do NOT try to `cat`, `head`, `strings`, or read it directly. Use only the MCP tools to query it. +- The **original source/project files might or might NOT be available on disk**. Project files (.csproj, .props, .targets, App.config, etc.) - if you cannot locate them on disk, they can only be read from within the binlog via MCP tools (e.g., embedded/source file retrieval). +- **Synthesize findings as you go.** Do not spend all available time investigating — once you have enough evidence, present your conclusions. A partial answer with clear reasoning is better than timing out mid-investigation. + +Use the available MCP server tools to query the binary log for: +- Build errors and warnings +- MSBuild properties and their values +- MSBuild items (PackageReference, ProjectReference, etc.) +- Project evaluation data +- Target execution details +- File contents embedded in the binlog + +## Fallback workflow — text-log replay (when MCP is unavailable) + +Use this only when the MCP server cannot be started (for example, on an older +SDK or in an offline environment without access to the `dotnet-tools` NuGet feed). + +### Replay the binlog to text logs ```bash dotnet msbuild build.binlog -noconlog \ @@ -21,90 +44,20 @@ dotnet msbuild build.binlog -noconlog \ -fl2 -flp2:warningsonly;logfile=warnings.log ``` -> **PowerShell note:** Use `-flp:"v=diag;logfile=full.log;performancesummary"` (quoted semicolons). +> **PowerShell note:** Use `-flp:"v=diag;logfile=full.log;performancesummary"` +> (quoted semicolons). -### Step 2: Read the errors +### Search the text logs ```bash cat errors.log -``` - -This gives all errors with file paths, line numbers, error codes, and project context. - -### Step 3: Search for context around specific errors - -```bash -# Find all occurrences of a specific error code with surrounding context grep -n -B2 -A2 "CS0246" full.log - -# Find which projects failed to compile grep -i "CoreCompile.*FAILED\|Build FAILED\|error MSB" full.log - -# Find project build order and results -grep "done building project\|Building with" full.log | head -50 -``` - -### Step 4: Detect cascading failures - -Projects that never reached `CoreCompile` failed because a dependency failed, not their own code: - -```bash -# List all projects that ran CoreCompile grep 'Target "CoreCompile"' full.log | grep -oP 'project "[^"]*"' - -# Compare against projects that had errors to identify cascading failures -grep "project.*FAILED" full.log ``` -### Step 5: Examine project files for root causes - -```bash -# Read the .csproj of the failing project -cat path/to/Services/Services.csproj - -# Check PackageReference and ProjectReference entries -grep -n "PackageReference\|ProjectReference" path/to/Services/Services.csproj -``` - -**Write your diagnosis as soon as you have enough information.** Do not over-investigate. - -## Additional Workflows - -### Performance Investigation -```bash -# The PerformanceSummary is at the end of full.log -tail -100 full.log # shows target/task timing summary -grep "Target Performance Summary\|Task Performance Summary" -A 50 full.log -``` - -### Dependency/Evaluation Issues -```bash -# Check evaluation properties -grep -i "OutputPath\|IntermediateOutputPath\|TargetFramework" full.log | head -30 -# Check item groups -grep "PackageReference\|ProjectReference" full.log | head -30 -``` - -## Replay reference - -| Command | Purpose | -|---------|---------| -| `dotnet msbuild X.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` | Full diagnostic log with perf summary | -| `dotnet msbuild X.binlog -noconlog -fl -flp:errorsonly;logfile=errors.log` | Errors only | -| `dotnet msbuild X.binlog -noconlog -fl -flp:warningsonly;logfile=warnings.log` | Warnings only | -| `grep -n "PATTERN" full.log` | Search for patterns in the replayed log | -| `dotnet msbuild -pp:preprocessed.xml Proj.csproj` | Preprocess — inline all imports into one file | - ## Generating a binlog (only if none exists) ```bash dotnet build /bl:build.binlog ``` - -## Common error patterns - -1. **CS0246 / "type not found"** → Missing PackageReference — check the .csproj -2. **MSB4019 / "imported project not found"** → SDK install or global.json issue -3. **NU1605 / "package downgrade"** → Version conflict in package graph -4. **MSB3277 / "version conflicts"** → Binding redirect or version alignment issue -5. **Project failed at ResolveProjectReferences** → Cascading failure from a dependency diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-parallelism/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-parallelism/SKILL.md index 3930391..b2dbaf6 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-parallelism/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-parallelism/SKILL.md @@ -1,6 +1,6 @@ --- name: build-parallelism -description: "Guide for optimizing MSBuild build parallelism and multi-project scheduling. Only activate in MSBuild/.NET build context. USE FOR: builds not utilizing all CPU cores, speeding up multi-project solutions, evaluating graph build mode (/graph), build time not improving with -m flag, understanding project dependency topology. Note: /maxcpucount default is 1 (sequential) — always use -m for parallel builds. Covers /maxcpucount, graph build for better scheduling and isolation, BuildInParallel on MSBuild task, reducing unnecessary ProjectReferences, solution filters (.slnf) for building subsets. DO NOT USE FOR: single-project builds, incremental build issues (use incremental-build), compilation slowness within a project (use build-perf-diagnostics), non-MSBuild build systems. INVOKES: dotnet build -m, dotnet build /graph, binlog analysis." +description: "Guide for optimizing MSBuild build parallelism and multi-project scheduling. Only activate in MSBuild/.NET build context. USE FOR: builds not utilizing all CPU cores, speeding up multi-project solutions, evaluating graph build mode (/graph), build time not improving with -m flag, understanding project dependency topology. Note: /maxcpucount default is 1 (sequential) — always use -m for parallel builds. Covers /maxcpucount, graph build for better scheduling and isolation, BuildInParallel on MSBuild task, reducing unnecessary ProjectReferences, solution filters (.slnf) for building subsets. DO NOT USE FOR: single-project builds, incremental build issues (use incremental-build), compilation slowness within a project (use build-perf-diagnostics), non-MSBuild build systems. INVOKES: binlog MCP server tools (expensive_projects, expensive_targets, project_target_times); falls back to dotnet build -m, dotnet build /graph, binlog replay + grep." license: MIT --- @@ -51,6 +51,18 @@ license: MIT ## Analyzing Parallelism with Binlog +### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace): + +1. Use expensive_projects tool → find the slowest projects and compare individual vs total build time +2. Use expensive_targets tool → find bottleneck targets +3. Use project_target_times tool → drill into a specific project's target-level timing +4. Ideal: build time should be much less than sum of project times (parallelism) +5. If build time ≈ sum of project times: too many serial dependencies, or one slow project blocking others + +### Fallback: text-log replay (when MCP is unavailable) + Step-by-step: 1. Replay the binlog: `dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary` diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md index 7b0d162..5dfad20 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md @@ -1,11 +1,16 @@ --- name: build-perf-diagnostics -description: "Diagnose MSBuild build performance bottlenecks using binary log analysis. Only activate in MSBuild/.NET build context. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline analysis, Target/Task Performance Summary interpretation, and 7 common bottleneck categories. Use after build-perf-baseline has established measurements. DO NOT USE FOR: establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems. INVOKES: dotnet msbuild binlog replay with performancesummary, grep for analysis." +description: "Diagnose MSBuild build performance bottlenecks using binary log analysis. Only activate in MSBuild/.NET build context. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline analysis, Target/Task Performance Summary interpretation, and 7 common bottleneck categories. Use after build-perf-baseline has established measurements. DO NOT USE FOR: establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, errors, search, items, properties); falls back to dotnet msbuild binlog replay + grep/cat when the MCP is unavailable." license: MIT --- ## Performance Analysis Methodology +1. **Generate a binlog**: `dotnet build /bl:{} -m` +2. Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) which is bundled with this plugin + +### Alternate flow when MCP is unavailable: binlog replay to text logs + 1. **Generate a binlog**: `dotnet build /bl:{} -m` 2. **Replay to diagnostic log with performance summary**: ```bash diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md index b7819f9..864ab78 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md @@ -1,6 +1,6 @@ --- name: check-bin-obj-clash -description: "Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. Only activate in MSBuild/.NET build context. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, missing outputs in multi-project builds, multi-targeting builds where project.assets.json conflicts. Diagnoses when multiple projects or TFMs write to the same bin/obj directories due to shared OutputPath, missing AppendTargetFrameworkToOutputPath, or extra global properties like PublishReadyToRun creating redundant evaluations. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems. INVOKES: dotnet msbuild binlog replay, grep for output path analysis." +description: "Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. Only activate in MSBuild/.NET build context. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, missing outputs in multi-project builds, multi-targeting builds where project.assets.json conflicts. Diagnoses when multiple projects or TFMs write to the same bin/obj directories due to shared OutputPath, missing AppendTargetFrameworkToOutputPath, or extra global properties like PublishReadyToRun creating redundant evaluations. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, projects, evaluations, properties, double_writes); falls back to dotnet msbuild binlog replay + grep when the MCP is unavailable." license: MIT --- @@ -35,13 +35,53 @@ Clashes can occur between: Use the `binlog-generation` skill to generate a binary log with the correct naming convention. -## Step 2: Replay the Binary Log to Text +## Primary workflow — binlog MCP + +The MCP server exposes structured tools for inspecting a `.binlog` without +parsing text logs. Call them directly instead of replaying the binlog to a text +file. Call `tools/list` for the MCP first if you are unsure which tools are available. + +**Important constraints:** +- The `.binlog` file is a **binary format** — do NOT try to `cat`, `head`, `strings`, or read it directly. Use only the MCP tools to query it. +- **Synthesize findings as you go.** Do not spend all available time investigating — once you have enough evidence, present your conclusions. + +### Step 2: Get an overview and list projects + +Use the MCP overview and projects tools to understand the build and list all projects that participated. + +### Step 3: Check evaluations and global properties + +Use the MCP `evaluations` and `evaluation_global_properties` tools to find all evaluations per project. Look for: +- Multiple evaluations for the same project (indicates multi-targeting or multiple build configurations) +- Differing global properties between evaluations (`TargetFramework`, `Configuration`, `RuntimeIdentifier`, `SolutionFileName`, `PublishReadyToRun`, etc.) + +### Step 4: Get output paths for each evaluation + +Use the MCP properties tool to query `OutputPath`, `IntermediateOutputPath`, `BaseOutputPath`, and `BaseIntermediateOutputPath` for each project evaluation. + +### Step 5: Check for double writes + +Use the MCP double_writes tool if available — it directly detects files written by multiple project instances. + +### Step 6: Identify clashes + +Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluations: +1. **Normalize paths** - Convert to absolute paths and normalize separators +2. **Group by path** - Find evaluations that share the same OutputPath or IntermediateOutputPath +3. **Filter out non-build evaluations** - Exclude `BuildProjectReferences=false` instances (P2P queries) +4. **Report clashes** - Any group with more than one evaluation indicates a clash + +## Fallback workflow — text-log replay (when MCP is unavailable) + +Use this only when the MCP server cannot be started. + +### Step 2: Replay the Binary Log to Text ```bash dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log ``` -## Step 3: List All Projects +### Step 3: List All Projects ```bash grep -i 'done building project\|Building project' full.log | grep -oP '"[^"]+\.csproj"' | sort -u @@ -49,7 +89,7 @@ grep -i 'done building project\|Building project' full.log | grep -oP '"[^"]+\.c This lists all project files that participated in the build. -## Step 4: Check for Multiple Evaluations per Project +### Step 4: Check for Multiple Evaluations per Project Multiple evaluations for the same project indicate multi-targeting or multiple build configurations: @@ -59,7 +99,7 @@ grep -c 'Evaluation started' full.log grep 'Evaluation started.*\.csproj' full.log ``` -## Step 5: Check Global Properties for Each Evaluation +### Step 5: Check Global Properties for Each Evaluation For each project, query the build properties to understand the build configuration: @@ -88,7 +128,7 @@ When analyzing clashes, filter evaluations based on the type of clash you're inv 3. **Always exclude `BuildProjectReferences=false`**: These are P2P metadata queries, not actual builds that write files. -## Step 6: Get Output Paths for Each Project +### Step 6: Get Output Paths for Each Project Query each project's output path properties: @@ -103,7 +143,7 @@ dotnet msbuild MyProject.csproj -getProperty:BaseOutputPath dotnet msbuild MyProject.csproj -getProperty:BaseIntermediateOutputPath ``` -## Step 7: Identify Clashes +### Step 7: Identify Clashes Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluations: @@ -111,7 +151,7 @@ Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluati 2. **Group by path** - Find evaluations that share the same OutputPath or IntermediateOutputPath 3. **Report clashes** - Any group with more than one evaluation indicates a clash -## Step 8: Verify Clashes via CopyFilesToOutputDirectory (Optional) +### Step 8: Verify Clashes via CopyFilesToOutputDirectory (Optional) As additional evidence for OutputPath clashes, check if multiple project builds execute the `CopyFilesToOutputDirectory` target to the same path. Note that not all clashes manifest here - compilation outputs and other targets may also conflict. @@ -129,7 +169,7 @@ Look for evidence of clashes in the messages: The `SkipUnchangedFiles` skip message often masks clashes - the build succeeds but is vulnerable to race conditions in parallel builds. -## Step 9: Check CoreCompile Execution Patterns (Optional) +### Step 9: Check CoreCompile Execution Patterns (Optional) To understand which project instance did the actual compilation vs redundant work, check `CoreCompile`: diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/eval-performance/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/eval-performance/SKILL.md index 9d45d2f..308d442 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/eval-performance/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/eval-performance/SKILL.md @@ -1,6 +1,6 @@ --- name: eval-performance -description: "Guide for diagnosing and improving MSBuild project evaluation performance. Only activate in MSBuild/.NET build context. USE FOR: builds slow before any compilation starts, high evaluation time in binlog analysis, expensive glob patterns walking large directories (node_modules, .git, bin/obj), deep import chains (>20 levels), preprocessed output >10K lines indicating heavy evaluation, property functions with file I/O ($([System.IO.File]::ReadAllText(...))), multiple evaluations per project. Covers the 5 MSBuild evaluation phases, glob optimization via DefaultItemExcludes, import chain analysis with /pp preprocessing. DO NOT USE FOR: compilation-time slowness (use build-perf-diagnostics), incremental build issues (use incremental-build), non-MSBuild build systems. INVOKES: dotnet msbuild -pp:full.xml for preprocessing, /clp:PerformanceSummary." +description: "Guide for diagnosing and improving MSBuild project evaluation performance. Only activate in MSBuild/.NET build context. USE FOR: builds slow before any compilation starts, high evaluation time in binlog analysis, expensive glob patterns walking large directories (node_modules, .git, bin/obj), deep import chains (>20 levels), preprocessed output >10K lines indicating heavy evaluation, property functions with file I/O ($([System.IO.File]::ReadAllText(...))), multiple evaluations per project. Covers the 5 MSBuild evaluation phases, glob optimization via DefaultItemExcludes, import chain analysis with /pp preprocessing. DO NOT USE FOR: compilation-time slowness (use build-perf-diagnostics), incremental build issues (use incremental-build), non-MSBuild build systems. INVOKES: binlog MCP server tools (evaluations, evaluation_global_properties, evaluation_properties, imports, properties); falls back to dotnet msbuild -pp:full.xml for preprocessing, /clp:PerformanceSummary." license: MIT --- @@ -18,6 +18,18 @@ Key insight: evaluation happens BEFORE any targets run. Slow evaluation = slow b ## Diagnosing Evaluation Performance +### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) to analyze evaluation performance: + +1. Use the evaluations tool to list all evaluations and their durations +2. Use evaluation_global_properties to check for multiple evaluations with differing global properties +3. Use evaluation_properties to inspect evaluated properties for a specific project+TFM +4. Use imports tool to analyze the import chain depth and structure +5. Use properties tool to check for expensive property function evaluations + +### Fallback: text-log replay and preprocessing (when MCP is unavailable) + ### Using binlog 1. Replay the binlog: `dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log` diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/extension-points/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/extension-points/SKILL.md new file mode 100644 index 0000000..463cc95 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/extension-points/SKILL.md @@ -0,0 +1,177 @@ +--- +name: extension-points +description: "Guide for MSBuild extensibility: CustomBefore/CustomAfter hooks, wildcard imports with alphabetic ordering, import gating with control properties, NuGet package build extension layout (build/buildTransitive), and the MicrosoftCommonPropsHasBeenImported guard. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing MSBuild import and hook patterns, reviewing and fixing extension point anti-patterns in Directory.Build files, fixing missing Exists() guards on imports that break fresh clones, fixing NuGet package hooks being silently dropped instead of appended, making build targets extensible for other projects, injecting custom logic into the build pipeline, creating NuGet packages that extend the build, conditionally disabling imports. DO NOT USE FOR: target authoring patterns (use target-authoring), props vs targets placement (use directory-build-organization), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# MSBuild Extension Points + +How the MSBuild pipeline provides hooks for SDKs, NuGet packages, repos, and users to inject custom logic. + +## CustomBefore / CustomAfter Hooks + +Every major `.targets` file defines import hooks: + +```xml + + + $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets + + + + + + +``` + +### Rules + +- Default path includes version (`v$(MSBuildToolsVersion)`) for side-by-side installations. +- Always check `Exists()`. The file may not be present on every machine. +- **Append** to the property (don't overwrite) to chain multiple hooks: + +```xml + + + $(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets + + +``` + +## Wildcard Import Directories + +MSBuild imports all files in extension directories, sorted alphabetically: + +```xml + +``` + +### Key paths + +| Property | Resolves to | Scope | +|---|---|---| +| `$(MSBuildUserExtensionsPath)` | `%APPDATA%\Microsoft\MSBuild` | Per-user | +| `$(MSBuildExtensionsPath)` | MSBuild install directory | Machine-wide | +| `$(MSBuildProjectExtensionsPath)` | `obj/` directory | Per-project (NuGet) | + +Name files with numeric prefixes for ordering: `01-first.props`, `02-second.props`. + +## Import Gating — Control Properties + +Every wildcard import is gated by a boolean property: + +```xml + + true + true + +``` + +### Available control properties + +| Property | What it disables | +|---|---| +| `ImportDirectoryBuildProps` | Directory.Build.props auto-discovery | +| `ImportDirectoryBuildTargets` | Directory.Build.targets auto-discovery | +| `ImportProjectExtensionProps` | NuGet-generated `*.props` in obj/ | +| `ImportProjectExtensionTargets` | NuGet-generated `*.targets` in obj/ | +| `ImportByWildcardBefore*` | Machine-level ImportBefore extensions | +| `ImportByWildcardAfter*` | Machine-level ImportAfter extensions | + +## NuGet Package Build Extension Layout + +NuGet packages inject build logic via `build/` or `buildTransitive/` folders: + +```text +MyPackage/ + build/ + MyPackage.props ← imported via *.props wildcard + MyPackage.targets ← imported via *.targets wildcard + buildTransitive/ + MyPackage.props ← imported by transitive consumers + MyPackage.targets +``` + +### Rules + +- File names **must match the package ID** exactly. +- `build/` affects direct consumers only. `buildTransitive/` affects the entire dependency chain. +- Props are imported early (before the project), targets are imported late (after the project). + +## Import Guard Pattern + +The `.targets` file ensures `.props` was imported using a guard property: + +```xml + + + true + + + + +``` + +This handles projects that only import `.targets`. + +## Directory.Build Discovery + +MSBuild walks up the directory tree to find the nearest `Directory.Build.props`: + +```xml +<_DirectoryBuildPropsBasePath> + $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props')) + +``` + +Only the **nearest** file is discovered. Nested hierarchies must explicitly import parents: + +```xml + + + <_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) + + +``` + +## Creating Your Own Extension Point + +```xml + + + + + + $(MSBuildProjectDirectory)\MySDK.Before.targets + $(MSBuildProjectDirectory)\MySDK.After.targets + + + + + + BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild + + + + + + + + + + +``` + +## Common Pitfalls + +- **Missing `Exists()` on optional imports** causes build failures when files are absent. +- **Overwriting Custom* properties** drops prior hooks. Append with `;` separator. +- **NuGet package file names not matching package ID** silently skips the import. +- **Nested Directory.Build.props** without parent import loses repo-root settings. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/incremental-build/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/incremental-build/SKILL.md index 9406553..3b1e7e7 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/incremental-build/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/incremental-build/SKILL.md @@ -1,6 +1,6 @@ --- name: incremental-build -description: "Guide for optimizing MSBuild incremental builds. Only activate in MSBuild/.NET build context. USE FOR: builds slower than expected on subsequent runs, 'nothing changed but it rebuilds anyway', diagnosing why targets re-execute unnecessarily, fixing broken no-op builds. Covers 8 common causes: missing Inputs/Outputs on custom targets, volatile properties in output paths (timestamps/GUIDs), file writes outside tracked Outputs, missing FileWrites registration, glob changes, Visual Studio Fast Up-to-Date Check (FUTDC) issues. Key diagnostic: look for 'Building target completely' vs 'Skipping target' in binlog. DO NOT USE FOR: first-time build slowness (use build-perf-baseline), parallelism issues (use build-parallelism), evaluation-phase slowness (use eval-performance), non-MSBuild build systems. INVOKES: dotnet build /bl, binlog replay with diagnostic verbosity." +description: "Guide for optimizing MSBuild incremental builds. Only activate in MSBuild/.NET build context. USE FOR: builds slower than expected on subsequent runs, 'nothing changed but it rebuilds anyway', diagnosing why targets re-execute unnecessarily, fixing broken no-op builds. Covers 8 common causes: missing Inputs/Outputs on custom targets, volatile properties in output paths (timestamps/GUIDs), file writes outside tracked Outputs, missing FileWrites registration, glob changes, Visual Studio Fast Up-to-Date Check (FUTDC) issues. Key diagnostic: look for 'Building target completely' vs 'Skipping target' in binlog. DO NOT USE FOR: first-time build slowness (use build-perf-baseline), parallelism issues (use build-parallelism), evaluation-phase slowness (use eval-performance), non-MSBuild build systems. INVOKES: binlog MCP server tools (overview, search, target details); falls back to dotnet build /bl, binlog replay with diagnostic verbosity." license: MIT --- @@ -58,6 +58,18 @@ Use binary logs (binlogs) to understand exactly why targets ran instead of being ``` The first build establishes the baseline. The second build is the one you want to be incremental. Analyze `second.binlog`. +### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace) to analyze the second binlog: + +1. Use the overview tool to check overall build status and duration +2. Use the search tool to find targets that executed vs were skipped — search for "Building target completely", "Building target incrementally", "Skipping target" +3. Use the search tool to find "is newer than output" messages that reveal which input file triggered a rebuild +4. Use target-related tools (target_reasons, project_targets) to inspect why specific targets ran +5. Use the expensive_targets tool to find targets that consumed the most time in the second build — these are your optimization targets + +### Fallback: text-log replay (when MCP is unavailable) + 2. **Replay the second binlog** to a diagnostic text log: ```shell dotnet msbuild second.binlog -noconlog -fl -flp:v=diag;logfile=second-full.log;performancesummary diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/item-management/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/item-management/SKILL.md new file mode 100644 index 0000000..8f44045 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/item-management/SKILL.md @@ -0,0 +1,163 @@ +--- +name: item-management +description: "Patterns for managing MSBuild item groups: Include/Remove/Update semantics, item metadata, batching with %(Metadata), transforms, per-item filtering, and cross-product batching pitfalls. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing item group anti-patterns in .csproj files, reviewing item management for correctness, fixing CS2002 duplicate file warnings from SDK globbing, fixing targets that run more times than expected due to cross-product batching, fixing Include vs Update misuse on SDK-globbed items, fixing FileWrites registration for generated file clean support, moving generated files to IntermediateOutputPath. DO NOT USE FOR: target chain architecture (use target-authoring), property patterns (use property-patterns), incrementality (use incremental-build), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# MSBuild Item Management Patterns + +Canonical patterns for working with item groups, from `Microsoft.Common.CurrentVersion.targets`. + +## Include / Remove / Update — Three Operations + +| Operation | Purpose | When to use | +|---|---|---| +| `Include` | Add new items to the group | Creating items with identity + metadata | +| `Remove` | Remove items matching a pattern | Excluding files or clearing a group | +| `Update` | Modify metadata on existing items | Adding/changing metadata without re-adding | + +### Include — Add Items + +```xml + + + true + + +``` + +### Remove — Subtract Items + +```xml + + + + + + <_CleanOrphanFileWrites Include="@(_CleanPriorFileWrites)" + Exclude="@(_CleanCurrentFileWrites)" /> + + + <_Temporary Remove="@(_Temporary)" /> + +``` + +### Update — Modify Existing Items + +```xml + + + true + Microsoft.CodeAnalysis.Collections.SR + + +``` + +`Update` does not add items — it only modifies items already in the group. + +## Item Batching — %(Metadata) + +When `%(Metadata)` appears in target attributes or task parameters, MSBuild **batches** execution per unique metadata value. + +### Target-level batching (Outputs) + +```xml + + + +``` + +### Task-level batching + +```xml + + +``` + +### Per-item filtering with Condition + +```xml + + <_ResxOutput Include="@(EmbeddedResource->'%(OutputResource)')" + Condition="'%(EmbeddedResource.WithCulture)' == 'false'" /> + +``` + +### Batching rules + +- `%(Metadata)` in `Condition` or `Outputs` → target batches per unique value. +- `%(Metadata)` in task parameters → task batches per unique value. +- **Do not mix `%()` from different item groups** in the same expression — this causes a cross-product (see Common Pitfalls). + +## Item Transforms — @(Item->'expression') + +Transforms create new item lists by applying an expression to each item: + +```xml + + + + + +``` + +## Exclude Pattern — Set Subtraction on Include + +```xml + + + +``` + +`Exclude` only works on `Include` — it cannot be used with `Update` or `Remove`. + +## Conditional Item Inclusion + +```xml + + + + + + + + + +``` + +## PrivateAssets on Tool/Analyzer Packages + +```xml + + + + +``` + +## Common Pitfalls + +### Cross-product batching + +Referencing `%(Metadata)` from two different item groups creates O(N×M) executions: + +```xml + + + + + +``` + +### Generated files in source tree + +Write to `$(IntermediateOutputPath)` (obj/), not the source directory. Source-tree generation pollutes version control and can cause duplicate compilation via globs. + +### Missing FileWrites + +Every file created during a target must be added to `@(FileWrites)` for `dotnet clean` support. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/property-patterns/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/property-patterns/SKILL.md new file mode 100644 index 0000000..eeb0136 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/property-patterns/SKILL.md @@ -0,0 +1,167 @@ +--- +name: property-patterns +description: "MSBuild property definition patterns: conditional defaults, composition/concatenation, path normalization, trailing slash handling, TFM detection helpers, and property evaluation order. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing MSBuild property definition issues in .props or .csproj files, reviewing and fixing shared property configuration anti-patterns, fixing DefineConstants or NoWarn being overwritten instead of appended, fixing unconditional property assignments that prevent project-level overrides, fixing unquoted conditions that fail when properties are empty, fixing hardcoded paths that break cross-platform builds, setting property defaults that can be overridden, understanding property evaluation order and last-write-wins semantics. DO NOT USE FOR: props vs targets placement (use directory-build-organization), item operations (use item-management), target structure (use target-authoring), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# MSBuild Property Patterns + +Canonical property definition and manipulation patterns from the MSBuild repository. + +## Conditional Defaults — The Foundational Pattern + +Set a property **only if not already set**, allowing callers to override: + +```xml + + Debug + AnyCPU + true + +``` + +### Rules + +- Always quote both sides: `'$(Prop)' == ''` +- In `.props`: creates overridable defaults. In `.targets`: creates fallbacks. +- Properties without the condition **cannot be overridden** by earlier imports. + +## Nested Conditional Groups + +Group related properties under a shared condition: + +```xml + + $(DefineConstants);FEATURE_APARTMENT_STATE + $(DefineConstants);FEATURE_APM + true + + + + true + $(DefineConstants);RUNTIME_TYPE_NETCORE + +``` + +Use the outer `Condition` on `PropertyGroup` to avoid repeating the same condition on every property. + +> **Warning:** `$(TargetFramework)` is empty in `.props` files for single-targeting projects until the project body is evaluated. Place `TargetFramework`-conditioned property groups in `.targets` files (or the project file itself), where the value is always available. + +## Composition — Semicolon Concatenation + +Properties that hold lists use semicolons. Always include the existing value when appending: + +```xml + + $(DefineConstants);MY_FEATURE + $(NoWarn);NU5131;IDE0005 + $(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild);netstandard2.0 + +``` + +## Path Normalization and Trailing Slashes + +```xml + + + $(OutDir)\ + + + + + $([MSBuild]::NormalizePath('$(TargetDir)', 'ref', '$(TargetFileName)')) + + + + + + $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)')) + + +``` + +### Preferred path functions + +| Function | Purpose | +|---|---| +| `$([MSBuild]::NormalizePath(...))` | Combine and normalize (cross-platform) | +| `$([System.IO.Path]::Combine(...))` | Combine path segments | +| `$([System.IO.Path]::IsPathRooted(...))` | Check if absolute | +| `HasTrailingSlash(...)` | Check for trailing slash | +| `$([MSBuild]::GetDirectoryNameOfFileAbove(...))` | Walk up directory tree | +| `$(MSBuildThisFileDirectory)` | Directory of current file | + +## Target Framework Detection Helpers + +```xml + + + true + + + + + true + + + + + $(DefineConstants);TEST_ISWINDOWS + +``` + +## Guard Properties + +Mark that a file has been imported to prevent double-imports: + +```xml + + + true + + + + +``` + +## Feature Gating by MSBuild Version + +```xml + + true + +``` + +## Fallback Chains + +Set via primary source first, then fall back: + +```xml + + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToDotNetFrameworkSdkFile('tlbexp.exe')) + $(_NetFxToolsDir)TlbExp.exe + +``` + +## Last Write Wins — Evaluation Order + +MSBuild evaluates properties top-to-bottom. The last assignment wins: + +```xml + +value1 + +value2 + +value3 +``` + +Properties in `.targets` (imported late) override properties in `.props` (imported early) and the project file. + +## Common Pitfalls + +- **Unquoted conditions** (`$(X)==true`) fail when the property is empty. Always quote both sides. +- **Overwriting DefineConstants** (`MY_CONST`) drops all prior constants. Always append with `$(DefineConstants);`. +- **Hardcoded absolute paths** break portability. Use `$(MSBuildThisFileDirectory)` or `$([MSBuild]::NormalizePath(...))`. +- **Missing `Condition` on defaults** makes properties non-overridable. Add `Condition="'$(Prop)' == ''"` for values meant to be defaults. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/resolve-project-references/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/resolve-project-references/SKILL.md index 62b979f..664e088 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/resolve-project-references/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/resolve-project-references/SKILL.md @@ -38,7 +38,13 @@ The reported time includes **waiting for dependent projects to build** while the ### Step 3: Redirect to task self-time -Guide the user to use the **Task** Performance Summary instead: +Use the **Task** Performance Summary to identify the real bottleneck. + +#### Primary: binlog MCP (preferred) + +Use the **binlog MCP server** expensive_tasks tool to get task self-time rankings directly from the binlog. + +#### Fallback: text-log replay (when MCP is unavailable) ```bash dotnet msbuild build.binlog -noconlog -fl "-flp:v=diag;logfile=full.log;performancesummary" diff --git a/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/target-authoring/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/target-authoring/SKILL.md new file mode 100644 index 0000000..4c0ace8 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-msbuild/skills/target-authoring/SKILL.md @@ -0,0 +1,161 @@ +--- +name: target-authoring +description: "Canonical patterns for writing custom MSBuild targets. Only activate in MSBuild/.NET build context. USE FOR: diagnosing and fixing custom target authoring anti-patterns, reviewing MSBuild target definitions for correctness, diagnosing broken SDK target chains across files (e.g., Directory.Build.targets silently redefining SDK targets), fixing targets that replace CompileDependsOn instead of extending it with $(CompileDependsOn), fixing query targets that return stale results due to Outputs vs Returns misuse, fixing missing Inputs/Outputs causing unnecessary rebuilds, fixing missing FileWrites registration. Covers DependsOnTargets vs BeforeTargets vs AfterTargets, the Build→CoreBuild three-level pattern, hooking into the build pipeline, the $(XxxDependsOn) chain-extension pattern. DO NOT USE FOR: incremental build tuning (use incremental-build), parallelization (use build-parallelism), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +license: MIT +--- + +# Custom Target Authoring Patterns + +Canonical patterns from `Microsoft.Common.CurrentVersion.targets` in the MSBuild repository. + +## The Three-Level Target Chain + +Every major entry point (Build, Rebuild, Clean) delegates to a **property** listing its dependencies, which chains through Before → Core → After: + +```xml + + + BeforeBuild; + CoreBuild; + AfterBuild + + + + + + + + +``` + +`CoreBuild` delegates to `$(CoreBuildDependsOn)` and includes error handlers: + +```xml + + + + +``` + +### Rules + +- Delegate to a property (`DependsOnTargets="$(MyTargetDependsOn)"`), not hardcoded targets. +- `OnError` goes inside the orchestrating target to ensure cleanup runs even on failure. +- Empty Before/After targets are extensibility points. Users override them; SDKs never put logic in them. + +## Chain Extension — Append, Never Overwrite + +When adding a custom target to an existing chain, **append** to the `DependsOn` property: + +```xml + + + $(CompileDependsOn);MyCodeGenTarget + + + + + MyCodeGenTarget + +``` + +## DependsOnTargets vs BeforeTargets vs AfterTargets + +| Mechanism | Defined in | Best for | +|---|---|---| +| `DependsOnTargets` | The target that needs deps | Target explicitly requires others | +| `BeforeTargets` | The injecting target | Insert before a target you don't own | +| `AfterTargets` | The injecting target | Insert after a target you don't own | + +Validation targets use `BeforeTargets` to intercept all entry points: + +```xml + + +``` + +**Rules:** + +- Use `DependsOnTargets` when your target needs specific prerequisites. +- Use `BeforeTargets`/`AfterTargets` when injecting into a pipeline you don't own. +- Prefer `BeforeTargets="CoreCompile"` over modifying `$(CompileDependsOn)` when you don't control the targets file. + +## Returns vs Outputs + +```xml + + + + + +``` + +- **`Returns`** specifies what the MSBuild task receives when calling this project. Use for inter-project communication. +- **`Outputs`** on inner targets is for incrementality (timestamp checks). Use for up-to-date detection. +- Never mix the two purposes. Query targets (`GetTargetPath`, `GetTargetFrameworks`) should use `Returns`, not `Outputs`. + +## Target Naming Conventions + +| Pattern | Meaning | Example | +|---|---|---| +| `_PrefixedName` | Internal/private target | `_TimeStampBeforeCompile` | +| `CoreXxx` | The actual implementation | `CoreBuild`, `CoreCompile` | +| `BeforeXxx` / `AfterXxx` | Empty extensibility hooks | `BeforeBuild`, `AfterCompile` | +| `PrepareXxx` | Setup/validation phase | `PrepareForBuild` | +| `ResolveXxx` | Discovery/resolution phase | `ResolveReferences` | +| `GetXxx` | Lightweight query (no side effects) | `GetTargetPath` | + +## Complete Custom Target Template + +```xml + + + + _ValidateMyFeatureInputs; + BeforeMyFeature; + CoreMyFeature; + AfterMyFeature + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## Common Pitfalls + +- **Overwriting `DependsOn` properties** drops SDK targets silently. Always include `$(ExistingProperty)` when appending. +- **Using `Outputs` on query targets** causes MSBuild to skip them when "up to date," returning stale data. Use `Returns`. +- **Defining targets in `.props`** means `BeforeTargets` on SDK targets have nothing to hook into yet. Move targets to `.targets`. +- **Forgetting `OnError`** in orchestrating targets means file tracking fails on build errors, breaking subsequent incremental builds. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/README.md b/external-sources/upstreams/dotnet-skills/dotnet-test/README.md new file mode 100644 index 0000000..731fdd4 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/README.md @@ -0,0 +1,103 @@ +# dotnet-test + +Skills and agents for running, generating, analyzing, migrating, and improving .NET tests across all major frameworks (MSTest, xUnit, NUnit, TUnit) and platforms (VSTest, Microsoft.Testing.Platform). + +## When to use this plugin + +- **Run tests** — execute `dotnet test` with automatic platform/framework detection and filter syntax +- **Generate tests** — scaffold comprehensive unit tests for any language via a multi-agent pipeline +- **Migrate tests** — upgrade MSTest v1/v2 → v3 → v4, xUnit v2 → v3, or VSTest → Microsoft.Testing.Platform +- **Audit test quality** — detect anti-patterns, test smells, assertion gaps, and coverage risks +- **Improve testability** — find static dependencies, generate wrappers, and migrate call sites to injectable abstractions +- **Measure coverage** — collect code coverage, compute CRAP scores, and surface risk hotspots + +## Skills + +### Test execution + +| Skill | Description | +|---|---| +| **run-tests** | Run .NET tests via `dotnet test` with platform/framework auto-detection and filter support | +| **mtp-hot-reload** | Rapid test-fix iteration using MTP hot reload (edit code → re-run without rebuilding) | + +### Test generation + +| Skill | Description | +|---|---| +| **code-testing-agent** | Multi-agent pipeline (Research → Plan → Implement → Build → Test → Fix → Lint) that generates tests for any language | +| **writing-mstest-tests** | Best practices and modern APIs for writing MSTest 3.x/4.x tests | + +### Test migration + +| Skill | Description | +|---|---| +| **migrate-mstest-v1v2-to-v3** | Upgrade MSTest v1 (assembly refs) or v2 (NuGet 1.x–2.x) to v3 | +| **migrate-mstest-v3-to-v4** | Upgrade MSTest v3 to v4 — handles all source and behavioral breaking changes | +| **migrate-xunit-to-xunit-v3** | Upgrade xUnit.net v2 to v3 | +| **migrate-vstest-to-mtp** | Migrate from VSTest runner to Microsoft.Testing.Platform | + +### Test quality & analysis + +| Skill | Description | +|---|---| +| **test-anti-patterns** | Quick pragmatic scan for ~15 common test quality issues with severity ranking | +| **test-smell-detection** | Deep formal audit using academic test smell taxonomy (19 smell types) | +| **assertion-quality** | Measure assertion variety and depth — find shallow tests that barely verify anything | +| **test-gap-analysis** | Pseudo-mutation analysis to find test blind spots that coverage numbers miss | +| **test-tagging** | Tag tests with standardized traits (smoke, regression, boundary, critical-path, etc.) | + +### Coverage & risk + +| Skill | Description | +|---|---| +| **coverage-analysis** | Project-wide code coverage collection with CRAP score computation and risk hotspot reporting | +| **crap-score** | Calculate CRAP (Change Risk Anti-Patterns) scores for individual methods, classes, or files | + +### Testability improvement + +| Skill | Description | +|---|---| +| **detect-static-dependencies** | Scan C# code for hard-to-test statics (DateTime.Now, File.*, HttpClient, etc.) | +| **generate-testability-wrappers** | Generate wrapper interfaces or guide adoption of built-in abstractions (TimeProvider, IFileSystem) | +| **migrate-static-to-wrapper** | Bulk-replace static call sites with injected wrapper calls and add constructor injection | + +### Reference data (loaded by other skills) + +| Skill | Description | +|---|---| +| **code-testing-extensions** | Language-specific guidance files loaded by the code-testing pipeline | +| **platform-detection** | Detect VSTest vs MTP and identify the test framework from project files | +| **filter-syntax** | Test filter syntax reference for VSTest and MTP across all frameworks | +| **dotnet-test-frameworks** | Framework detection patterns, assertion APIs, skip annotations, and lifecycle methods | + +## Agents + +### User-facing agents + +These are the entry-point agents you invoke directly: + +| Agent | Purpose | +|---|---| +| **code-testing-generator** | Orchestrates the full test generation pipeline (research → plan → implement → build → test → fix → lint) | +| **test-migration** | Auto-detects framework/version and routes to the correct migration skill | +| **test-quality-auditor** | Runs multi-skill audit pipelines for comprehensive test suite assessment | +| **testability-migration** | End-to-end testability improvement: detect → generate wrappers → migrate call sites | + +### Internal subagents + +These are pipeline stages invoked automatically by the agents above (`user-invocable: false`). You do not need to call them directly: + +| Agent | Called by | Purpose | +|---|---|---| +| **code-testing-researcher** | code-testing-generator | Analyzes codebase structure, testing patterns, and testability | +| **code-testing-planner** | code-testing-generator | Creates phased test implementation plans from research findings | +| **code-testing-implementer** | code-testing-generator | Implements one phase from the plan, runs build-test-fix cycles | +| **code-testing-builder** | code-testing-implementer | Runs build/compile commands and reports results | +| **code-testing-tester** | code-testing-implementer | Runs test commands and reports pass/fail results | +| **code-testing-fixer** | code-testing-implementer | Fixes compilation errors in source or test files | +| **code-testing-linter** | code-testing-implementer | Runs code formatting and linting | + +## Prerequisites + +- .NET SDK installed (`dotnet` on PATH) +- A project with an existing test framework (MSTest, xUnit, NUnit, or TUnit) for execution and analysis skills diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md index ab4722a..1e08d22 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md @@ -2,9 +2,10 @@ description: >- Orchestrates comprehensive test generation using Research-Plan-Implement pipeline. Use when asked to generate tests, write unit - tests, improve test coverage, or add tests. + tests, improve test coverage, or add tests. DO NOT USE FOR: diagnosing + coverage plateaus or project-wide coverage/CRAP analysis without writing tests + (use coverage-analysis); targeted method/class CRAP scores (use crap-score). name: code-testing-generator -tools: ['read', 'search', 'edit', 'task', 'skill', 'terminal'] license: MIT --- @@ -34,7 +35,7 @@ Based on the request scope, pick exactly one strategy and follow it: | Strategy | When to use | What to do | | ---------- | ------------- | ------------ | -| **Direct** | A small, self-contained request (e.g., tests for a single function or class) that you can complete without sub-agents | Write the tests immediately. **Run them right away** — if any test fails, read the production code, fix the assertion, and re-run before writing more tests. Skip Steps 3-5 (research, plan, implement sub-agents). Then proceed to Steps 6-9 for validation and reporting. | +| **Direct** | A small, self-contained request (e.g., tests for a single function or class) that you can complete without sub-agents | Follow the codebase conventions on test file structure, naming, style, and testing approaches. Reuse existing test projects and test files when possible — if the code under test already has tests, add new tests to the same file or test project. Only create a new test file when no canonical file is named or discoverable for the symbol under test. Write the tests immediately. **Run them right away** — if any test fails, read the production code, fix the assertion, and re-run before writing more tests. Skip Steps 3-5 (research, plan, implement sub-agents). Then proceed to Steps 6-9 for validation and reporting. | | **Single pass** | A moderate scope (couple projects or modules) that a single Research → Plan → Implement cycle can cover | Execute Steps 3-8 once, then proceed to Step 9. | | **Iterative** | A large scope or ambitious coverage target that one pass cannot satisfy | Execute Steps 3-8, then re-evaluate coverage. If the target is not met, repeat Steps 3-8 with a narrowed focus on remaining gaps. Use unique names for each iteration's `.testagent/` documents (e.g., `research-2.md`, `plan-2.md`) so earlier results are not overwritten. Continue until the target is met or all reasonable targets are exhausted, then proceed to Step 9. | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md index 85c8fcb..1099744 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md @@ -78,5 +78,5 @@ Failures: - Include file:line references when available - **For .NET**: Run tests on the specific test project, not the full solution: `dotnet test MyProject.Tests.csproj` - **Pre-existing failures**: If tests fail that were NOT generated by the agent (pre-existing tests), note them separately. Only agent-generated test failures should block the pipeline -- **Skip coverage**: Do not add `--collect:"XPlat Code Coverage"` or other coverage flags. Coverage collection is not the agent's responsibility +- **Skip coverage by default**: Do not add `--collect:"XPlat Code Coverage"` or other coverage flags to the test command — coverage collection is not the agent's responsibility. **Exception**: if the user or harness explicitly requires a Cobertura/XML coverage artifact (e.g., they ask for `coverlet.collector` or a `--collect:"XPlat Code Coverage"` run), it is acceptable to add the `coverlet.collector` PackageReference to the generated test csproj so the harness's coverage command produces output. Do not run the coverage command yourself; leave that to the validation step - **Failure analysis for generated tests**: When reporting failures in freshly generated tests, note that these tests have never passed before. The most likely cause is incorrect test expectations (wrong expected values, wrong mock setup), not production code bugs diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-migration.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-migration.agent.md index d218c74..1b1a11c 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-migration.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-migration.agent.md @@ -6,7 +6,6 @@ description: >- and guides users through end-to-end upgrades. Use when asked to upgrade MSTest, migrate to xUnit v3, switch to Microsoft.Testing.Platform, modernize test infrastructure, or when the user says "migrate my tests". -tools: ['read', 'search', 'edit', 'terminal', 'skill'] user-invokable: true disable-model-invocation: false handoffs: diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-quality-auditor.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-quality-auditor.agent.md index 0913620..9d5065b 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-quality-auditor.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/test-quality-auditor.agent.md @@ -9,7 +9,6 @@ description: >- running multiple analysis skills in sequence. Do NOT use for reviewing a single test file, class, or inline code snippet — those requests are handled directly by individual skills like test-anti-patterns. -tools: ['read', 'search', 'edit', 'terminal', 'skill'] user-invokable: true disable-model-invocation: false handoffs: @@ -60,13 +59,13 @@ Classify the user's request and route to the appropriate skill: | User Intent | Route To | Plugin | |---|---|---| -| "Are my assertions good enough?" / shallow testing / assertion diversity | `exp-assertion-quality` skill | dotnet-experimental | -| "Find test smells" / comprehensive formal audit | `exp-test-smell-detection` skill | dotnet-experimental | +| "Are my assertions good enough?" / shallow testing / assertion diversity | `assertion-quality` skill | dotnet-test | +| "Find test smells" / comprehensive formal audit | `test-smell-detection` skill | dotnet-test | | "Pragmatic anti-pattern check" within a broader audit context | `test-anti-patterns` skill | dotnet-test | | "Find test duplication" / boilerplate / DRY up tests | `exp-test-maintainability` skill | dotnet-experimental | | "Are my mocks needed?" / over-mocking / mock audit | `exp-mock-usage-analysis` skill | dotnet-experimental | -| "Would my tests catch bugs?" / mutation analysis / test gaps | `exp-test-gap-analysis` skill | dotnet-experimental | -| "Categorize my tests" / tag tests / trait distribution | `exp-test-tagging` skill | dotnet-experimental | +| "Would my tests catch bugs?" / mutation analysis / test gaps | `test-gap-analysis` skill | dotnet-test | +| "Categorize my tests" / tag tests / trait distribution | `test-tagging` skill | dotnet-test | | "Coverage report" / risk hotspots / CRAP score | `coverage-analysis` skill (use `crap-score` only for explicitly targeted method/class CRAP analysis or narrow-scope Cobertura data) | dotnet-test | | "Find untestable code" / static dependencies | `detect-static-dependencies` skill → hand off to `testability-migration` agent for fixes | dotnet-test | | "Full health check" / "audit my tests" / broad quality request | Run the **Comprehensive Audit Pipeline** below | multiple | @@ -83,11 +82,11 @@ Run these in order. Each step builds context for the next. Stop early if the use - Quick pragmatic scan for the most impactful issues - Produces severity-ranked findings (Critical → Low) -2. **Assertion quality** — `exp-assertion-quality` skill +2. **Assertion quality** — `assertion-quality` skill - Measures assertion variety and depth - Reveals whether tests actually verify meaningful behavior -3. **Test gaps** — `exp-test-gap-analysis` skill +3. **Test gaps** — `test-gap-analysis` skill - Pseudo-mutation analysis to find blind spots - Answers "would tests catch a bug here?" @@ -97,10 +96,10 @@ Run these in order. Each step builds context for the next. Stop early if the use ### Optional follow-ups (offer but don't run automatically) -5. **Test smells** — `exp-test-smell-detection` skill (if step 1 found many issues and the user wants a deeper formal audit) +5. **Test smells** — `test-smell-detection` skill (if step 1 found many issues and the user wants a deeper formal audit) 6. **Maintainability** — `exp-test-maintainability` skill (if the test suite is large and duplication is suspected) 7. **Mock audit** — `exp-mock-usage-analysis` skill (if over-mocking was flagged in step 1) -8. **Test tagging** — `exp-test-tagging` skill (if the user wants to understand test type distribution) +8. **Test tagging** — `test-tagging` skill (if the user wants to understand test type distribution) ### Synthesizing results @@ -152,4 +151,4 @@ Prioritize findings by impact: - **Always start with detection**: Identify test framework, test project paths, and approximate test count before diving into analysis - **Lead with actionable findings**: Put the most impactful issues first - **Distinguish analysis from action**: This agent produces reports. If the user wants to fix issues, point them to the appropriate skill or agent (e.g., `testability-migration` for static dependencies, `code-testing-generator` for writing new tests) -- **Be honest about experimental skills**: Skills from `dotnet-experimental` are being refined — mention this context when presenting their results +- **Be honest about experimental skills**: Skills from `dotnet-experimental` (`exp-test-maintainability`, `exp-mock-usage-analysis`) are being refined — mention this context when presenting their results diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/testability-migration.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/testability-migration.agent.md index 87f75c9..f280226 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/testability-migration.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/testability-migration.agent.md @@ -6,7 +6,6 @@ description: >- Use when asked to make code testable, remove static coupling, migrate to TimeProvider, adopt IFileSystem, or improve testability of a legacy codebase. name: testability-migration -tools: ['read', 'search', 'edit', 'terminal', 'skill'] handoffs: - label: Generate Tests for Migrated Code agent: code-testing-generator diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-assertion-quality/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md similarity index 85% rename from external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-assertion-quality/SKILL.md rename to external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md index 551fa71..e3770ba 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-assertion-quality/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md @@ -1,6 +1,6 @@ --- -name: exp-assertion-quality -description: "Analyzes the variety and depth of assertions across .NET test suites. Use when the user asks to evaluate assertion quality, find shallow testing, identify tests with only trivial assertions, measure assertion coverage diversity, or audit whether tests verify different facets of correctness. Produces metrics and actionable recommendations. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), detecting anti-patterns (use test-anti-patterns), or fixing existing assertions." +name: assertion-quality +description: "Analyzes the variety and depth of assertions across .NET test suites. Use when the user asks to evaluate assertion quality, find shallow testing, identify assertion-free tests (no assertions or only trivial ones like Assert.IsNotNull), flag self-referential or tautological assertions (output equals input on identity/round-trip operations), measure assertion coverage diversity, or audit whether tests verify different facets of correctness. Produces metrics and actionable recommendations. Works with MSTest, xUnit, NUnit, TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), other anti-patterns like flakiness or duplication (use test-anti-patterns), or fixing assertions." license: MIT --- @@ -47,7 +47,7 @@ Low assertion diversity signals shallow testing. Tests may pass while bugs hide ### Step 1: Gather the test code -Read all test files the user provides. If the user points to a directory or project, scan for all test files — see the `exp-dotnet-test-frameworks` skill for framework-specific markers. +Read all test files the user provides. If the user points to a directory or project, scan for all test files — see the `dotnet-test-frameworks` skill for framework-specific markers. ### Step 2: Classify every assertion @@ -83,6 +83,7 @@ Calculate these metrics for the test suite: - **Assertion type spread**: Number of distinct assertion categories used across the suite (out of 12) - **Tests with zero assertions**: Count and percentage of test methods with no assertions at all - **Tests with only trivial assertions**: Count and percentage of tests where every assertion is only a null check or `Assert.IsTrue(true)` — trivial means no meaningful value verification +- **Tests with self-referential assertions**: Count and percentage of tests whose assertions compare an input to a round-tripped or identity-transformed version of itself (e.g., `Assert.AreEqual(input, Parse(input.ToString()))`) or assert a field against itself (`Assert.AreEqual(dto.Name, dto.Name)`). These are tautological — they verify the plumbing, not the behavior. - **Tests with negative assertions**: Count and percentage (target: at least 10% of tests should verify what should NOT happen) - **Tests with exception assertions**: Count and percentage - **Tests with state/side-effect assertions**: Count and percentage @@ -98,6 +99,7 @@ Before reporting, calibrate findings: - **Consider the test's intent.** A test for a void method that verifies state change on a dependency is legitimate even if it only uses `Assert.IsTrue`. - **Exception tests are inherently low-assertion-count.** `Assert.ThrowsException(() => ...)` may be the only assertion — that's fine for exception-focused tests. Don't penalize them for low assertion count. - **Don't conflate diversity with volume.** A test with 20 `Assert.AreEqual` calls has high volume but low diversity. A test with one equality, one null check, and one exception assertion has low volume but good diversity. +- **Self-referential assertions are not meaningful equality checks.** `Assert.AreEqual(input, roundTrip(input))` looks like a real equality assertion but is tautological when the operation under test is expected to be identity. Flag these separately from normal equality assertions. If the test's *purpose* is to verify a round-trip (serialize/deserialize, encode/decode), the assertion is valid — but it should be accompanied by assertions on non-trivial inputs that exercise the transformation. - **If assertions are well-diversified, say so.** A report concluding the suite has good diversity is perfectly valid. ### Step 5: Report findings diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-agent/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-agent/SKILL.md index 1250a47..b0e2e49 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-agent/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-agent/SKILL.md @@ -1,15 +1,20 @@ --- name: code-testing-agent description: >- - Generates comprehensive, workable unit tests for any programming language - using a multi-agent pipeline. Use when asked to generate tests, write unit - tests, improve test coverage, add test coverage, or create test files. - Supports C#, TypeScript, JavaScript, Python, Go, Rust, Java, and more. - Orchestrates research, planning, and implementation phases to produce - tests that compile, pass, and follow project conventions. - DO NOT USE FOR: running existing tests, executing dotnet test, applying - test filters, detecting test platforms, or troubleshooting test execution - (use run-tests for all of these). + Generates and writes new unit tests for any programming language using a + Research-Plan-Implement pipeline. Use when asked to generate tests, + write unit tests, add tests, improve test coverage, create test + project, achieve high coverage, comprehensive tests, or asked to + scaffold a new test project for an app, service, or library. Supports + C#, TypeScript, JavaScript, Python, Go, Rust, Java, and more. Orchestrates + the code-testing-generator sub-agent through research, planning, and + implementation phases so tests compile, pass, and follow project + conventions. DO NOT USE FOR: running existing tests or test filters + (use run-tests); diagnosing coverage plateaus or project-wide + coverage/CRAP analysis without writing tests (use coverage-analysis); + targeted method/class CRAP scores (use crap-score); MSTest assertion + guidance, MSTest test pattern modernization, or fixing existing MSTest test + code (use writing-mstest-tests). license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/SKILL.md index cd74bfe..1874b3f 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/SKILL.md @@ -18,7 +18,16 @@ This skill provides access to language-specific guidance files used by the code- | File | Language | Contents | |------|----------|----------| | [extensions/dotnet.md](extensions/dotnet.md) | .NET (C#/F#/VB) | Build commands, test commands, project reference validation, common CS error codes, MSTest template | +| [extensions/python.md](extensions/python.md) | Python | Framework-adaptive test commands (pytest, custom runners), project layout detection, mocking guidelines, common errors | +| [extensions/typescript.md](extensions/typescript.md) | TypeScript/JavaScript | Build/test commands (Jest/Vitest/Mocha), framework detection, mocking, TS-specific considerations | +| [extensions/powershell.md](extensions/powershell.md) | PowerShell | Test commands (Pester v5), module import patterns, discovery/run pitfalls, mocking, common errors | | [extensions/cpp.md](extensions/cpp.md) | C++ | Testing internals with friend declarations | +| [extensions/go.md](extensions/go.md) | Go | `go test` commands, table-driven tests, integration vs unit layout, mocking via interfaces, common errors | +| [extensions/java.md](extensions/java.md) | Java | Maven/Gradle commands, JUnit 4/5 and TestNG detection, Mockito, Spring Boot slices, common errors | +| [extensions/rust.md](extensions/rust.md) | Rust | `cargo test` commands, unit vs integration vs doc tests, features, async test harnesses, common errors | +| [extensions/ruby.md](extensions/ruby.md) | Ruby | RSpec and Minitest commands, Bundler usage, Rails specifics, mocking patterns, common errors | +| [extensions/swift.md](extensions/swift.md) | Swift | SPM and Xcode test commands, XCTest vs Swift Testing, `@testable import`, async/throws tests, common errors | +| [extensions/kotlin.md](extensions/kotlin.md) | Kotlin | Gradle commands, JUnit/Kotest detection, MockK, coroutines test, KMP and Android specifics, common errors | | [extensions/dotnet-examples.md](extensions/dotnet-examples.md) | .NET (C#/F#/VB) | Concrete pipeline examples: sample research output, plan, generated tests, fix cycles, final report | ## Usage diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/dotnet.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/dotnet.md index 7c30a78..666fdb3 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/dotnet.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/dotnet.md @@ -71,6 +71,18 @@ If a new test project was created, register it with the solution so `dotnet test 4. Skip this if the project is already included in the solution or solution filter used for testing. 5. Prefer the researched test command. If you need to run the solution directly, use `dotnet test --solution ` only for repos on .NET SDK 10+ with MTP-style syntax; otherwise use the standard positional form `dotnet test `. +## Test Framework Detection + +Detect the framework from the test project's `.csproj` package references and match its conventions: + +| Package Reference | Framework | Attributes | Assertion Style | +|-------------------|-----------|------------|-----------------| +| `MSTest.Sdk` or `MSTest.TestFramework` | MSTest | `[TestClass]`, `[TestMethod]`, `[DataRow]` | `Assert.AreEqual(expected, actual)` | +| `xunit` | xUnit | `[Fact]`, `[Theory]`, `[InlineData]` | `Assert.Equal(expected, actual)` | +| `NUnit` | NUnit | `[TestFixture]`, `[Test]`, `[TestCase]` | `Assert.That(actual, Is.EqualTo(expected))` | + +Use the repo's existing framework — do not introduce a different one. + ## MSTest Template ```csharp @@ -110,4 +122,6 @@ public sealed class ClassNameTests ## Skip Coverage Tools -Do not configure or run code coverage measurement tools (coverlet, dotnet-coverage, XPlat Code Coverage). These tools have inconsistent cross-configuration behavior and waste significant time. Coverage is measured separately by the evaluation harness. +Do not configure or run code coverage measurement tools (coverlet, dotnet-coverage, XPlat Code Coverage) by default. These tools have inconsistent cross-configuration behavior and waste significant time. Coverage is measured separately by the evaluation harness. + +**Exception**: if the user or evaluation harness explicitly requires a Cobertura/XML coverage artifact (e.g., they ask for `coverlet.collector` or a `--collect:"XPlat Code Coverage"` run), add the `coverlet.collector` PackageReference to the generated .NET test csproj so the harness's coverage command can produce output. Do not run the coverage command yourself; leave that to the validation step. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/go.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/go.md new file mode 100644 index 0000000..f084767 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/go.md @@ -0,0 +1,158 @@ +# Go Extension + +Language-specific guidance for Go test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*_test.go` files and copy their style (table-driven layout, helper usage, assertion library, build tags) +2. **`go.mod` / `go.sum`** — module path, Go version, dependencies (e.g. `testify`, `gomock`, `mockery`) +3. **Build/CI scripts** — `Makefile`, `magefile.go`, `Taskfile.yml`, `.github/workflows/*.yml` +4. **`go.work`** — if present, you are in a workspace; tests for a module must run from that module's directory or use `-C` (Go 1.20+) + +Use whatever assertion style and test layout the repo already uses. Do not introduce `testify` if the repo uses the standard library only. + +## Toolchain Detection + +| Indicator | Meaning | +|-----------|---------| +| `go.mod` `go 1.x` directive | Minimum Go version — match it locally with `go version` | +| `go.work` at the root | Multi-module workspace; commands resolve dependent modules from sibling directories | +| `vendor/` directory | Vendored deps; many commands implicitly add `-mod=vendor` | +| `tools.go` with `//go:build tools` | Tool versions pinned in `go.mod` (e.g. `mockgen`); install with `go install` from the listed paths | + +## Build Commands + +| Scope | Command | +|-------|---------| +| Compile a package | `go build ./path/to/pkg` | +| Vet (static analysis) | `go vet ./...` | +| Compile tests without running | `go test -count=1 -run=^$ ./path/to/pkg` | +| Whole module | `go build ./...` | + +`go build ./...` is the closest thing to a "does it compile" gate. It does not exercise test files — use `go test -run=^$` to type-check tests as well. + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests in a package | `go test ./path/to/pkg` | +| All tests in module | `go test ./...` | +| Single test | `go test -run '^TestName$' ./path/to/pkg` | +| Subtest | `go test -run '^TestName$/^subname$' ./path/to/pkg` | +| Verbose | `go test -v ./path/to/pkg` | +| Race detector | `go test -race ./...` | +| Disable cache | `go test -count=1 ./...` | +| Short mode | `go test -short ./...` | + +- `-run` arguments are **regular expressions anchored** with `^...$`; without anchors the pattern matches as a substring +- `go test -count=1` is the canonical way to bypass the test result cache; never use a fake `-count=2` or environment hacks +- `-race` significantly slows tests and requires CGO — only enable if the repo's CI does + +## Lint Command + +Use the repo's lint script first (`make lint`, `task lint`). Otherwise detect from `.golangci.yml`/`.golangci.yaml`: + +- `.golangci.yml` present → `golangci-lint run ./...` +- No config → `gofmt -w .` and `go vet ./...` +- `goimports` config / pre-commit hook → `goimports -w path/to/file.go` + +Never disable existing linters in the test files you generate. + +## Project Layout and Imports + +Go uses package paths derived from the module path in `go.mod`. + +| Scenario | Test placement | Package declaration | +|----------|----------------|----------------------| +| Internal-only test (white-box) | `foo_test.go` next to `foo.go` | `package foo` (same as production) | +| External-only test (black-box) | `foo_test.go` next to `foo.go` | `package foo_test` (forces use of public API) | +| Integration / build-tag gated | `foo_integration_test.go` | Add `//go:build integration` at top | + +- Test files **must** end with `_test.go` — the toolchain ignores other names +- A package directory may contain both `package foo` and `package foo_test` test files simultaneously +- Helpers shared across tests in one package go in `helpers_test.go` — do not export them; put them in the `_test` package only if integration tests in another package need them +- Imports use the full module path: `import "github.com/org/module/pkg"` — copy the exact module path from `go.mod` + +## Test Function Signatures + +| Kind | Signature | +|------|-----------| +| Standard test | `func TestThing(t *testing.T)` | +| Subtests | `t.Run("name", func(t *testing.T) { ... })` | +| Benchmark | `func BenchmarkThing(b *testing.B)` | +| Example (godoc) | `func ExampleThing()` with `// Output:` comment | +| Fuzz (Go 1.18+) | `func FuzzThing(f *testing.F)` | +| Per-package setup | `func TestMain(m *testing.M)` — call `m.Run()` and `os.Exit` with its code | + +Use **table-driven tests** when generating multiple cases for the same behavior — this is idiomatic Go and matches what most repos already use: + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + want int + }{ + {"positives", 2, 3, 5}, + {"negatives", -1, -1, -2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Add(tt.a, tt.b); got != tt.want { + t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want) + } + }) + } +} +``` + +When iterating with `t.Run` over a loop variable on Go < 1.22, capture it with `tt := tt` to avoid closure-over-loop-variable bugs. + +## Common Errors + +| Error | Fix | +|-------|-----| +| `package X is not in std` / `cannot find module providing package X` | Add the import to `go.mod`: `go get path/to/module@version`, then `go mod tidy` | +| `import cycle not allowed in test` | Move shared helpers to a separate package, or switch to a `_test` package for black-box tests | +| `undefined: X` in `_test` package | The symbol is unexported; either use `package foo` (white-box) or export it intentionally | +| `t.Parallel called multiple times` | Each subtest can call `t.Parallel()` once; do not call it twice in the same test | +| `panic: test executed panic(nil) or runtime.Goexit` | A goroutine called `t.Fatal` outside the test goroutine; only the main test goroutine may call `Fatal`/`FailNow` | +| `flag provided but not defined: -X` | Flags registered in `init()` of test files must use `flag.NewFlagSet` carefully; place test-only flags in `TestMain` | +| `go: cannot find main module` | Run inside the module directory (where `go.mod` lives), or use `-C path` (Go 1.20+) | +| `build constraints exclude all Go files in...` | Build tags filtered out every file — match the repo's tag with `-tags=integration` etc. | +| `missing go.sum entry for module` | Run `go mod download` or `go mod tidy` | +| Race detector reports data race | Fix the race; do not silence it. CGO must be enabled | + +## Mocking Rules + +Go has no reflection-based mocking framework that's universally adopted. Pick what the repo already uses: + +- **Interfaces + hand-written fakes** (most idiomatic) — define a small interface in the consumer package and pass a struct that implements it +- **`gomock` / `mockgen`** — if the repo has `//go:generate mockgen ...` directives or `mocks/` directories, regenerate via `go generate ./...` rather than editing generated files +- **`testify/mock`** — used in many repos; instantiate with `new(MockX)` and chain `.On("Method", ...).Return(...)` +- **`httptest`** — for HTTP clients/servers; spin up `httptest.NewServer` instead of mocking `http.Client` + +Always prefer dependency injection over global function patching. If a test needs more than 3 mocks, flag it as a design smell. + +## Concurrency and Cleanup + +- Use `t.Cleanup(func() { ... })` instead of deferring in test bodies — runs even if `t.FailNow` fires +- Use `t.TempDir()` for temp files — auto-cleaned at test end +- Use `t.Context()` (Go 1.24+) or pass an explicit `context.Background()` — never call real network or filesystem APIs without one in long-running tests + +## Dependency Installation (Last Resort) + +Only install packages after investigation confirms they are missing: + +``` +go get github.com/stretchr/testify@latest +go mod tidy +``` + +Run `go mod tidy` after any `go get` to keep `go.sum` consistent. Never edit `go.sum` by hand. + +## Skip Coverage Tools + +Do not configure or run coverage tools (`-cover`, `-coverprofile`, `go tool cover`). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/java.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/java.md new file mode 100644 index 0000000..2a3e74d --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/java.md @@ -0,0 +1,198 @@ +# Java Extension + +Language-specific guidance for Java test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*Test.java` / `*Tests.java` / `*IT.java` (integration) files and copy their style (JUnit version, assertion library, mock library, lifecycle methods) +2. **Build file** — `pom.xml` (Maven), `build.gradle` / `build.gradle.kts` (Gradle), `BUILD` / `BUILD.bazel` (Bazel) +3. **Java version** — ``, `sourceCompatibility`, or `toolchains` block +4. **Wrapper scripts** — always prefer `./mvnw` or `./gradlew` over a system-installed Maven/Gradle so you match the project's pinned version + +Use whatever framework the repo already uses (JUnit 4, JUnit 5/Jupiter, TestNG). Do not migrate to a different framework as a side effect of writing tests. + +## Build Tool Detection + +| Indicator | Build tool | Default test command | +|-----------|------------|----------------------| +| `pom.xml` | Maven | `./mvnw test` | +| `build.gradle` / `build.gradle.kts` | Gradle | `./gradlew test` | +| `settings.gradle*` with `include 'subproject'` | Gradle multi-project | `./gradlew :subproject:test` | +| `BUILD` / `BUILD.bazel` | Bazel | `bazel test //path/to:test` | + +If both `pom.xml` and `build.gradle` exist, pick the one used by CI. + +## Build Commands + +| Scope | Maven | Gradle | +|-------|-------|--------| +| Compile main + test | `./mvnw test-compile` | `./gradlew testClasses` | +| Compile only | `./mvnw compile` | `./gradlew classes` | +| Full build | `./mvnw verify` | `./gradlew build` | +| Skip tests during build | `./mvnw -DskipTests package` | `./gradlew assemble` | + +- Use `-q` (Maven) / `--console=plain` (Gradle) to reduce output noise +- For Gradle, prefer `--no-daemon` only in CI; locally the daemon makes incremental builds far faster + +## Test Commands + +| Scope | Maven | Gradle | +|-------|-------|--------| +| All unit tests | `./mvnw test` | `./gradlew test` | +| Single class | `./mvnw test -Dtest=MyClassTest` | `./gradlew test --tests MyClassTest` | +| Single method | `./mvnw test -Dtest=MyClassTest#myMethod` | `./gradlew test --tests MyClassTest.myMethod` | +| Tag filter (JUnit 5) | `./mvnw test -Dgroups=fast` | `./gradlew test -PincludeTags=fast` (if configured) or `--tests` | +| Integration tests | `./mvnw verify -DskipUnitTests` (with failsafe-plugin) | `./gradlew integrationTest` (if registered) | + +- `Surefire` runs unit tests (`*Test.java`); `Failsafe` runs integration tests (`*IT.java`) — do not put long integration tests under Surefire +- Gradle's `--tests` accepts wildcards: `--tests "*MyMethod*"` +- Use `--rerun-tasks` (Gradle) or `-DforkCount=...` (Surefire) only when troubleshooting cache issues + +## Lint Command + +Use the repo's existing lint task first. Otherwise check for: + +- Checkstyle (`checkstyle.xml`, `checkstyle`) → `./mvnw checkstyle:check` or `./gradlew checkstyleMain` +- Spotless (`spotless` block / plugin) → `./mvnw spotless:apply` or `./gradlew spotlessApply` +- ErrorProne / NullAway → integrated into compilation; run a normal build +- google-java-format / palantir-java-format → use the repo's configured formatter + +Never disable existing checks in the test files you generate. + +## Project Layout and Imports + +Maven/Gradle conventional layout: + +``` +src/ +├── main/java/com/example/foo/Bar.java +├── main/resources/ +├── test/java/com/example/foo/BarTest.java +└── test/resources/ +``` + +| Layout | Test placement | +|--------|----------------| +| Standard | `src/test/java//Test.java` | +| Integration tests separated | `src/integrationTest/java/...` (Gradle) or `src/it/java/...` (Maven w/ failsafe) | +| Multi-module Maven | Tests live in the same module as the code under test | + +- Test classes must mirror the production class's **package** to access package-private members +- Avoid wildcard imports unless the repo already uses them — match the explicit imports shown in the templates below +- For JUnit 5: import `org.junit.jupiter.api.Test` (and other annotations as needed) and `org.junit.jupiter.api.Assertions.assertEquals` etc. as static imports +- For JUnit 4: import `org.junit.Test`, `org.junit.Before`, etc., and `org.junit.Assert.assertEquals` etc. as static imports + +## Test Framework Detection + +| Indicator | Framework | Annotations | Assertion style | +|-----------|-----------|-------------|------------------| +| `junit-jupiter-*` deps | JUnit 5 | `@Test`, `@ParameterizedTest`, `@BeforeEach`, `@DisplayName` | `Assertions.assertEquals(expected, actual)` | +| `junit:junit:4.x` | JUnit 4 | `@Test`, `@Before`, `@RunWith` | `Assert.assertEquals(expected, actual)` | +| `org.testng:testng` | TestNG | `@Test(groups=...)`, `@BeforeMethod` | `Assert.assertEquals(actual, expected)` (note **reversed** order) | +| `org.assertj:assertj-core` | AssertJ (assertions only) | n/a | `assertThat(actual).isEqualTo(expected)` | +| `org.hamcrest:hamcrest` | Hamcrest matchers | n/a | `assertThat(actual, is(equalTo(expected)))` | + +**Argument order matters**: JUnit/AssertJ use `(expected, actual)`; TestNG uses `(actual, expected)`. Reversing them produces confusing failure messages. + +## JUnit 5 Template + +```java +package com.example.foo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CalculatorTest { + + @Test + @DisplayName("add returns sum of two positive numbers") + void add_positiveNumbers_returnsSum() { + Calculator sut = new Calculator(); + assertEquals(5, sut.add(2, 3)); + } + + @ParameterizedTest + @CsvSource({ + "2, 3, 5", + "-1, 1, 0" + }) + void add_validInputs_returnsSum(int a, int b, int expected) { + assertEquals(expected, new Calculator().add(a, b)); + } + + @Test + void divide_byZero_throws() { + Calculator sut = new Calculator(); + assertThrows(ArithmeticException.class, () -> sut.divide(1, 0)); + } +} +``` + +## Common Errors + +| Error | Fix | +|-------|-----| +| `package X does not exist` | Add the dependency to `pom.xml` / `build.gradle`; run `./mvnw dependency:resolve` or `./gradlew --refresh-dependencies` | +| `cannot find symbol` | Verify class name and import path; check that the test source set sees the production source set | +| `No tests found for given includes` (Gradle) | `--tests` pattern doesn't match; verify the class/method names, that test methods are annotated with `@Test`, and that the class name matches the test task's `include` pattern (default `**/*Test*.class`). For JUnit 4 only, the class must also be `public` with a public no-arg constructor — JUnit 5 allows package-private classes and methods | +| `Test class should have exactly one public zero-argument constructor` (JUnit 4) | Remove constructors with parameters; use `@Before` for setup | +| `org.junit.runners.model.InvalidTestClassError` (JUnit 4) | Class is missing `public`, has wrong constructor, or method signature is wrong | +| Mixing `org.junit.Test` (4) and `org.junit.jupiter.api.Test` (5) | Pick one framework per test class — imports must match the framework annotation | +| `java.lang.NoClassDefFoundError` at runtime | Test runtime classpath is missing a transitive dep; add it to `testRuntimeOnly` (Gradle) or `test` (Maven) | +| `UnsupportedClassVersionError` | JDK used to run tests is older than the JDK used to compile; align toolchains | +| `Mockito cannot mock final class` | Use Mockito's inline mock maker — Mockito 5+ uses it by default; for Mockito 3.x/4.x add the `mockito-inline` artifact (replaces `mockito-core`). Or switch to MockK for Kotlin. `mockito-subclass` does **not** mock final classes | +| `WrongTypeOfReturnValue` (Mockito) | The stubbed method returns a different type than the mock was set up for — check return type signatures | + +## Mocking Rules + +- Use whatever the repo already uses: **Mockito** (most common), **EasyMock**, **JMockit**, or hand-written fakes +- For JUnit 5 + Mockito, use `@ExtendWith(MockitoExtension.class)` with `@Mock` / `@InjectMocks` fields +- For JUnit 4 + Mockito, use `@RunWith(MockitoJUnitRunner.class)` or `MockitoAnnotations.openMocks(this)` in `@Before` +- Use `when(mock.method(...)).thenReturn(...)` for stubs and `verify(mock).method(...)` for interactions +- Use `ArgumentCaptor` to assert on complex argument values rather than over-specifying matchers +- Prefer constructor injection so production code stays testable without `@InjectMocks` +- If a test needs more than 3 mocks, flag it as a design smell + +## Spring Boot + +If the repo uses Spring Boot: + +- `@SpringBootTest` loads the full context — slow; use only when needed +- Slice tests are faster: `@WebMvcTest`, `@DataJpaTest`, `@JsonTest` +- Use `@MockBean` (Spring) only inside Spring tests; in plain unit tests use `@Mock` +- Use `@Testcontainers` for real-DB integration tests if the repo already has it on the classpath + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing. + +Maven (`pom.xml`): + +```xml + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + +``` + +Gradle (`build.gradle.kts`): + +```kotlin +testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") +testRuntimeOnly("org.junit.platform:junit-platform-launcher") +``` + +If the repo uses BOMs (`` or Gradle platforms), reuse them — don't pin a different version than the BOM publishes. + +## Skip Coverage Tools + +Do not configure or run coverage tools (JaCoCo, Cobertura, OpenClover). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/kotlin.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/kotlin.md new file mode 100644 index 0000000..1bebd0a --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/kotlin.md @@ -0,0 +1,227 @@ +# Kotlin Extension + +Language-specific guidance for Kotlin test generation. For pure-Java codebases, use `java.md` instead. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find files in `src/test/kotlin/`, `src/commonTest/kotlin/`, `src/jvmTest/kotlin/`, etc., and copy their style (framework, assertion library, mock library, coroutine helpers) +2. **Build file** — `build.gradle.kts` / `build.gradle` — note Kotlin version, plugins (`kotlin("jvm")`, `kotlin("multiplatform")`, `kotlin("android")`), and `dependencies { testImplementation(...) }` +3. **`gradle/libs.versions.toml`** — the version catalog if the repo uses one; reference aliases instead of hard-coded versions +4. **Wrapper script** — always invoke `./gradlew` (Unix) or `.\gradlew.bat` (Windows), never a system-installed Gradle +5. **Multiplatform layout** — `src//kotlin/` indicates KMP; tests live in matching `*Test` source sets (`commonTest`, `jvmTest`, `nativeTest`) + +Use whatever framework the repo already uses (JUnit Jupiter, JUnit 4, Kotest, kotlin.test). Do not switch. + +## Project Type Detection + +| Indicator | Project type | +|-----------|--------------| +| `kotlin("jvm")` plugin | Plain JVM Kotlin | +| `kotlin("multiplatform")` plugin with `kotlin { jvm(); js(); ... }` | Kotlin Multiplatform | +| `com.android.application` / `com.android.library` plugin | Android | +| `org.springframework.boot` plugin | Spring Boot Kotlin | +| `kotlin("jvm")` + `application` plugin | Kotlin CLI / server | + +For **Android**, see also platform-specific test types: `src/test/` for unit tests on the JVM, `src/androidTest/` for instrumented tests on a device/emulator. They use different runners and gradle tasks. + +## Build Commands + +| Scope | Command | +|-------|---------| +| Compile main + test (JVM) | `./gradlew compileTestKotlin` | +| Full build | `./gradlew build` | +| Skip tests | `./gradlew assemble` | +| Single module | `./gradlew :module-name:build` | +| KMP target only | `./gradlew :module:jvmTest` (or `linuxX64Test`, etc.) | + +- Use `--console=plain` to suppress Gradle's animated output +- Use `--build-cache` (often default in CI) to reuse outputs +- For Android: `./gradlew assembleDebug` (build APK) and `./gradlew testDebugUnitTest` (run unit tests) + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests (JVM) | `./gradlew test` | +| Single class | `./gradlew test --tests "com.example.WidgetTest"` | +| Single method | `./gradlew test --tests "com.example.WidgetTest.add returns sum"` | +| KMP all targets | `./gradlew allTests` | +| KMP one target | `./gradlew jvmTest`, `./gradlew jsTest`, `./gradlew linuxX64Test` | +| Android unit tests | `./gradlew testDebugUnitTest` | +| Android instrumented | `./gradlew connectedDebugAndroidTest` (requires device/emulator) | + +- `--tests` accepts wildcards: `--tests "*Widget*"`. Method names with spaces or backticks must be quoted: `--tests "com.example.WidgetTest.creates a widget"` +- Use `--rerun-tasks` only when troubleshooting cache issues +- For Kotest, the runner is registered with JUnit Platform — the standard `./gradlew test` and `--tests` flags work the same way + +## Lint Command + +Use the repo's lint tooling first: + +- `./gradlew ktlintCheck` (autoformat: `./gradlew ktlintFormat`) when ktlint is configured +- `./gradlew detekt` when detekt is configured +- `./gradlew spotlessCheck` / `spotlessApply` for the Spotless plugin +- Android Studio's IDE inspections; `./gradlew lint` (Android-only) for the Android Lint task + +## Project Layout + +``` +src/ +├── main/kotlin/com/example/foo/Bar.kt +├── main/resources/ +├── test/kotlin/com/example/foo/BarTest.kt # mirrors production package +└── test/resources/ +``` + +KMP layout: + +``` +src/ +├── commonMain/kotlin/... # shared +├── commonTest/kotlin/... # shared tests using kotlin.test +├── jvmMain/kotlin/... +├── jvmTest/kotlin/... +├── jsMain/kotlin/... +└── jsTest/kotlin/... +``` + +- Test classes mirror the production class's package so they can access `internal` members (Kotlin's `internal` is module-scoped — within the same Gradle module, including the test source set) +- For KMP common tests, you can only import from `kotlin.test` and other multiplatform-aware libraries (e.g. `kotlinx.coroutines.test`, Kotest multiplatform, MockK on JVM only) + +## Test Framework Detection + +| Dependency | Framework | Annotations / DSL | +|------------|-----------|--------------------| +| `org.jetbrains.kotlin:kotlin-test` | kotlin.test (multiplatform) | `@Test`, `@BeforeTest`, `assertEquals`, `assertFailsWith` | +| `junit-jupiter-*` | JUnit 5 | `@Test`, `@ParameterizedTest`, `@BeforeEach`, `@DisplayName` | +| `junit:junit:4.x` | JUnit 4 | `@Test`, `@Before`, `@RunWith(JUnitPlatform::class)` rare | +| `io.kotest:kotest-runner-junit5` | Kotest | `class FooSpec : FunSpec({ test("...") { ... } })` (DSL — many styles: `StringSpec`, `BehaviorSpec`, etc.) | +| `org.spekframework.spek2:spek-dsl-jvm` | Spek 2 | `object FooSpec : Spek({ describe(...) { it(...) {} } })` (legacy) | + +For Kotest, **stick to the spec style the repo already uses** — mixing styles is confusing. + +## Test Templates + +### JUnit 5 + +```kotlin +package com.example.foo + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals + +class CalculatorTest { + + @Test + @DisplayName("add returns sum of two positive numbers") + fun `add returns sum of two positives`() { + val sut = Calculator() + assertEquals(5, sut.add(2, 3)) + } + + @Test + fun `divide by zero throws`() { + val sut = Calculator() + assertThrows { sut.divide(1, 0) } + } +} +``` + +Backticked method names (`` `like this` ``) are idiomatic for Kotlin tests because they read better in failure messages. + +### Kotest (StringSpec) + +```kotlin +package com.example.foo + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow + +class CalculatorSpec : StringSpec({ + "add returns sum of two positive numbers" { + Calculator().add(2, 3) shouldBe 5 + } + + "divide by zero throws" { + shouldThrow { Calculator().divide(1, 0) } + } +}) +``` + +## Coroutines + +- Use `kotlinx-coroutines-test` when it's already on the classpath; otherwise add it as a `testImplementation` only after confirming it is missing (see Dependency Installation) +- Use `runTest { ... }` (replaces the older `runBlockingTest`) for `suspend` test bodies +- For virtual time advance, use a `TestDispatcher` built from `testScheduler` — e.g. `StandardTestDispatcher(testScheduler)` or `UnconfinedTestDispatcher(testScheduler)` — rather than calling `delay` and waiting in real time +- Inject a `CoroutineDispatcher` into production code instead of using `Dispatchers.Main/IO` directly — then swap it in tests via `Dispatchers.setMain(testDispatcher)` + +```kotlin +@Test +fun `loads data eventually`() = runTest { + val repo = FakeRepo() + val dispatcher = StandardTestDispatcher(testScheduler) + val sut = Loader(repo, dispatcher) + sut.start() + advanceUntilIdle() + assertEquals(LoadState.Done, sut.state.value) +} +``` + +## Common Errors + +| Error | Fix | +|-------|-----| +| `Unresolved reference: X` | Add the import; verify the test source set sees the production source set; for KMP, the dep may be declared only in `jvmTest` | +| `Cannot access 'X': it is internal in module Y` | `internal` is module-scoped, so a test in another Gradle module cannot see it. Move the test into the same module, expose a public seam (e.g. a `*-testing` artifact, or change visibility deliberately), or add the consuming module to the source module's `friend modules` via the Kotlin compiler `-Xfriend-paths` option. `@VisibleForTesting` does **not** widen Kotlin visibility | +| `Class 'XTest' is not abstract and does not implement abstract member` (Kotest spec) | The spec class needs a no-arg constructor and a primary-constructor block — match the existing spec style | +| `No tests found for given includes` (Gradle) | `--tests` pattern doesn't match; verify class name and that the framework's runner is registered on the test task (`useJUnitPlatform()`) | +| `kotlin.UninitializedPropertyAccessException: lateinit property X has not been initialized` | The `@BeforeEach` (or `BeforeTest`) didn't run, or the field was reset; use `lateinit` only after confirming the lifecycle hook fires | +| `IllegalStateException: Module with the Main dispatcher had failed to initialize` | Coroutines test needs `Dispatchers.setMain(...)` before launching anything that touches `Dispatchers.Main`; reset with `Dispatchers.resetMain()` in teardown | +| `Mockito cannot mock final class` | Kotlin classes are `final` by default — either use **MockK** (works with final classes) or apply the `kotlin-allopen` plugin scoped to a marker annotation | +| `MissingMockKException` | The mock wasn't initialized; call `MockKAnnotations.init(this)` or use `@MockK` with `@MockKExtension` (JUnit 5) | +| KMP common test references a JVM-only API | Move the test to `jvmTest`, or use `expect/actual` declarations | +| Android: `Method ... not mocked` | The unit test runs on the JVM and the SDK class is just a stub — either use Robolectric, move the test to instrumented (`androidTest`), or refactor to inject the dependency | + +## Mocking Rules + +- **MockK** is the de-facto standard for Kotlin (final classes, coroutine support): `every { mock.foo() } returns 1`, `coEvery { mock.suspendFn() } returns 1`, `verify { mock.foo() }`, `coVerify { ... }` +- Mockito works on Kotlin too with `mockito-kotlin` extensions, but Kotlin classes are `final` by default — use Mockito's inline mock maker (default in Mockito 5+; the `mockito-inline` artifact for Mockito 3.x/4.x). `mockito-subclass` cannot mock final classes +- Avoid `mockkStatic`/`mockkObject` for production code you control — refactor to a wrapper instead +- Prefer constructor injection so you don't need framework annotations (`@InjectMocks`) at all +- If a test needs more than 3 mocks, flag it as a design smell + +## Android Specifics + +- Robolectric tests live under `src/test/` and emulate the Android framework on the JVM — fast but imperfect +- Instrumented tests live under `src/androidTest/`, require a connected device/emulator, and are slow — use sparingly +- Compose UI tests use `createComposeRule()` and `composeTestRule.onNodeWithText(...).performClick()` — match the existing test setup if Compose is in the project +- Hilt: use `@HiltAndroidTest` and `HiltAndroidRule` for instrumented tests; for unit tests pass fakes directly to ViewModels + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing. + +`build.gradle.kts`: + +```kotlin +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testImplementation("io.mockk:mockk:1.13.10") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") +} + +tasks.test { + useJUnitPlatform() +} +``` + +If the repo uses a version catalog, add to `gradle/libs.versions.toml` and reference via `libs.junit.jupiter` etc. Match the major versions already in use. + +## Skip Coverage Tools + +Do not configure or run coverage tools (JaCoCo, Kover). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/powershell.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/powershell.md new file mode 100644 index 0000000..e0b1201 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/powershell.md @@ -0,0 +1,110 @@ +# PowerShell Extension + +Language-specific guidance for PowerShell test generation using Pester v5. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*.Tests.ps1` files and copy their style (structure, assertions, mock approach, import method) +2. **Module structure** — look for `.psd1` (manifest), `.psm1` (root module), `Public/`/`Private/` organization +3. **Build/test scripts** — check for `build.ps1`, `Invoke-Build` (`*.build.ps1`), `psake`, or CI scripts +4. **Shell target** — check `.psd1` for `PowerShellVersion`/`CompatiblePSEditions`, CI matrix for `pwsh` vs `powershell.exe` + +Use the repo's existing test conventions. Only add Pester if the repo has no tests at all. + +## Build Commands + +PowerShell is interpreted — no build step. If the repo has a build script, use it. Otherwise validate with: + +- **Module loads**: `Import-Module ./MyModule.psd1 -Force -ErrorAction Stop` +- **Script analyzer**: `Invoke-ScriptAnalyzer -Path ./src -Recurse` (if PSScriptAnalyzer is available) +- **Lint**: `Invoke-ScriptAnalyzer -Path path/to/file.ps1 -Fix` + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests | `Invoke-Pester` | +| Specific file | `Invoke-Pester -Path ./Tests/Get-Widget.Tests.ps1` | +| Filter by name | `Invoke-Pester -FullNameFilter '*Get-Widget*'` | +| Filter by tag | `Invoke-Pester -TagFilter 'Unit'` | +| Non-interactive (CI) | `Invoke-Pester -CI` | +| Detailed output | `Invoke-Pester -Output Detailed` | + +- Prefer the repo's build/test script over raw `Invoke-Pester` +- Use `-Output Detailed` during fix cycles, `-Output Minimal` for final validation + +## Project Layout and Imports + +| Layout | Import in `BeforeAll` | +|--------|-----------------------| +| Module (`.psd1`) | `Import-Module "$PSScriptRoot/../MyModule.psd1" -Force` | +| Library script (defines functions) | `. $PSScriptRoot/Get-Widget.ps1` | +| Co-located test | `. $PSCommandPath.Replace('.Tests.ps1', '.ps1')` | +| Executable script (has `param()`) | Do **not** dot-source — invoke with `& $PSScriptRoot/script.ps1 -Param value` and assert on output/errors | + +- **All imports go in `BeforeAll`** — never at script top level +- **Use `$PSScriptRoot` or `$PSCommandPath`** — never `$MyInvocation.MyCommand.Path` (returns empty in `BeforeAll`) +- Use `-Force` on `Import-Module` to pick up changes between runs + +## Test File Naming + +- Files: `*.Tests.ps1` — match existing convention (co-located vs `Tests/` directory) + +## Pester v5 Discovery vs Run (Critical) + +Pester v5 runs in **two phases**: Discovery (collects test metadata) then Run (executes tests). This is the #1 source of agent errors. + +**Rules:** +- All setup code goes in `BeforeAll` or `BeforeEach` — never at script top level or loose inside `Describe`/`Context` +- Code directly inside `Describe`/`Context` (but outside `It`/`Before*`/`After*`) runs during **Discovery** — do not put setup, imports, or variable assignments there +- Data for `-ForEach` / `-TestCases` must be set in `BeforeDiscovery`, not `BeforeAll` (BeforeAll runs after discovery) +- `-Skip:$condition` evaluates at Discovery time — conditions from `BeforeAll` will be `$null` +- Use `foreach` loops for dynamic test generation only with `BeforeDiscovery` data +- Use `TestDrive:` for file-based tests instead of touching repo files — Pester cleans it up automatically + +## Common Errors + +| Error | Fix | +|-------|-----| +| Variable is `$null` in `It` block | Move assignment into `BeforeAll` — variables set there are visible to child `It` blocks without `$script:` | +| `-ForEach` data is empty | Move data setup from `BeforeAll` to `BeforeDiscovery` | +| `CommandNotFoundException` for Mock target | The function must exist before mocking — import the module in `BeforeAll` first | +| `$MyInvocation.MyCommand.Path` returns empty | Use `$PSCommandPath` or `$PSScriptRoot` instead | +| `Should Be` (no dash) fails | Use v5 syntax: `Should -Be` (with dash prefix) | +| `Assert-MockCalled` not recognized | Use v5 syntax: `Should -Invoke` | +| Mock has no effect | Check scope — mocks in `It` only apply to that `It`; use `BeforeAll`/`BeforeEach` for broader scope | +| `Should -Throw` doesn't catch cmdlet errors | Most cmdlet errors are non-terminating — wrap with `{ cmd -ErrorAction Stop }` or set `$ErrorActionPreference = 'Stop'` in `BeforeEach` | +| Tests pass on Windows but fail on Linux | Use `Join-Path` not string concatenation; match exact file casing; avoid Windows-only cmdlets (Registry, EventLog) | + +## Mocking Rules + +- Place mocks in `BeforeAll` (shared) or `BeforeEach` (reset per test) +- Mock where the command is **called from** — use `-ModuleName` to mock inside a module's scope +- Use `-ParameterFilter` for selective mocking (no `param()` block needed in v5) +- Verify calls with `Should -Invoke` — default scope inside `It` counts only that test's calls +- Use `InModuleScope` sparingly and as narrowly as possible — prefer `Mock -ModuleName` for testing via public API +- Inside mock bodies, use `$PesterBoundParameters` not `$PSBoundParameters` +- If a test needs more than 3 mocks, flag it as a design smell + +## Non-Obvious Assertions + +Most `Should` operators are self-explanatory. These are the ones agents get wrong: + +- `Should -Throw` requires a **scriptblock**: `{ risky-op } | Should -Throw` — not a direct call +- `Should -Contain` is for **collections** — use `Should -Be` for scalar equality +- `Should -HaveParameter` validates cmdlet signatures: `Get-Command X | Should -HaveParameter 'Name' -Mandatory` +- `Should -Invoke` verifies mock calls: `Should -Invoke Get-Item -Times 1 -Exactly` + +## Cross-Platform + +- Prefer `pwsh` (PowerShell 7+) unless the repo explicitly targets Windows PowerShell 5.1 +- Use `Join-Path` for paths — never string concatenation with `\` +- Linux/macOS file systems are **case-sensitive** — match exact casing in imports and paths +- Windows ships Pester 3.4.0 — if v5 is needed: `Install-Module Pester -Force -SkipPublisherCheck` +- Check `$PSVersionTable.PSEdition` to detect Core vs Desktop + +## Skip Coverage Tools + +Do not configure or run coverage tools (Pester CodeCoverage, JaCoCo export). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/python.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/python.md new file mode 100644 index 0000000..6584e52 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/python.md @@ -0,0 +1,132 @@ +# Python Extension + +Language-specific guidance for Python test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, discover what the repo already does: + +1. **Find ALL existing test files** — search broadly: `test_*.py`, `*_test.py`, `*.uts`, `test/*.sh`, or any other test format. Do not assume pytest. +2. **Identify the test framework** — look for: + - Custom test runners (e.g. `UTscapy` for scapy, project-specific harnesses) + - Standard frameworks (`pytest`, `unittest`, `nose2`) + - Test runner scripts in `Makefile`, `tox.ini`, `nox`, `scripts/` + - Config entries in `pyproject.toml`, `setup.cfg`, `pytest.ini`, `conftest.py` +3. **Read existing tests thoroughly** — copy their exact style: file format, imports, fixtures, assertion patterns, helper utilities, setup/teardown conventions +4. **Package layout** — determine import paths from existing code, not guesswork + +**Use whatever framework and conventions the repo already uses.** If the repo uses a custom test framework (custom file formats, custom runners, domain-specific test utilities), adopt it fully — do not layer pytest on top. Only introduce pytest if the repo has no tests at all. + +## Environment Detection + +Detect the runner from lockfiles/config and prefix all commands accordingly: + +| Indicator | Prefix | +|-----------|--------| +| `poetry.lock` / `[tool.poetry]` in `pyproject.toml` | `poetry run` | +| `pdm.lock` / `[tool.pdm]` in `pyproject.toml` | `pdm run` | +| `uv.lock` / `[tool.uv]` in `pyproject.toml` | `uv run` | +| `Pipfile.lock` | `pipenv run` | +| `hatch.toml` / `[tool.hatch]` in `pyproject.toml` | `hatch run` | +| None of the above | `python -m` | + +If `Makefile`, `tox.ini`, or `nox` config exists, prefer those scripts over raw commands. + +## Build Commands + +Python has no separate build step. Validate with the type checker if one is configured: + +| Scope | Command | +|-------|---------| +| Syntax check | ` py_compile path/to/file.py` | +| Type check | ` mypy path/to/file.py` or ` pyright path/to/file.py` | + +## Test Commands + +If the repo uses a **custom test framework** (custom file formats, custom runner), use its native commands — do not wrap them in pytest. Examples: + +| Framework | Command | +|-----------|---------| +| UTscapy (`.uts` files) | ` scapy.tools.UTscapy -f test/test_file.uts` | +| Custom runner script | `make test`, `./run_tests.sh`, `tox` | +| Repo-defined script | Whatever `scripts.test` in Makefile/tox/nox specifies | + +For **pytest** projects (the most common case), use the detected ``: + +| Scope | Command | +|-------|---------| +| All tests | ` pytest` | +| Specific file | ` pytest tests/test_module.py` | +| Specific test | ` pytest tests/test_module.py::TestClass::test_method` | +| Keyword filter | ` pytest -k "keyword"` | +| Stop on first failure | ` pytest -x --tb=short` | + +- Prefer `python -m pytest` over bare `pytest` to ensure the correct interpreter +- If the project uses `unittest` only (no pytest in deps), use `python -m unittest discover` + +## Lint Command + +Use the repo's existing lint script first (`make lint`, `tox -e lint`). Otherwise detect tools from config: + +- `ruff.toml` or `[tool.ruff]` → ` ruff check --fix && ruff format` +- `[tool.black]` → ` black` +- `.flake8` → ` flake8` + +## Project Layout and Imports + +| Layout | Import Style | +|--------|-------------| +| `src/package/module.py` | `from package.module import X` | +| `package/module.py` at root | `from package.module import X` | +| `module.py` at root | `from module import X` | + +- **Match existing test imports exactly** — do not invent `src.` prefixes unless existing tests use them +- Check `pyproject.toml` `[tool.setuptools.package-dir]` for layout hints +- Default test placement: `tests/` mirroring source structure (`src/billing/service.py` → `tests/billing/test_service.py`) + +## Test File Naming + +Match the repo's existing conventions. Common patterns: + +- **pytest**: Files `test_*.py` or `*_test.py`, functions `test_` prefix, classes `Test` prefix +- **Custom frameworks**: Use whatever format existing tests use (e.g. `.uts` for UTscapy, custom extensions) + +If writing new tests in a repo with no tests, default to pytest conventions. + +## Common Errors + +| Error | Fix | +|-------|-----| +| `ModuleNotFoundError: No module named 'src'` | Import from the package name used by the repo, not from `src` | +| `ModuleNotFoundError: No module named 'X'` | Check existing imports for the correct package name; if editable install needed: ` pip install -e .` | +| `ImportError: attempted relative import` | Convert to absolute imports matching existing test patterns | +| `fixture 'X' not found` | Check `conftest.py` for existing fixtures; reuse them instead of creating new ones | +| `TypeError: missing required argument` | Read the full `__init__`/function signature; pass all required parameters | +| `async def functions are not natively supported` | Use `@pytest.mark.asyncio` only if `pytest-asyncio` is already in deps; check for `asyncio_mode = "auto"` in config | +| `SyntaxError` | Fix syntax at the indicated line | + +## Mocking Rules + +- Use `unittest.mock` (stdlib) — no extra dependency needed +- **Patch where the name is looked up**, not where it is defined: `@patch("mypackage.module.datetime")` not `@patch("datetime.datetime")` +- Use `Mock(spec=RealClass)` to catch attribute errors +- Use `AsyncMock` for async functions +- Prefer dependency injection over `@patch` +- If a test needs more than 3 mocks, flag it as a design smell + +## Dependency Installation (Last Resort) + +Only install packages after investigation confirms they are missing. Use the detected prefix: + +| Manager | Install command | +|---------|----------------| +| Poetry | `poetry add --group dev pytest` | +| PDM | `pdm add -dG test pytest` | +| uv | `uv add --dev pytest` | +| pip | `python -m pip install -e ".[dev]"` | + +Never run bare `pip install` in a Poetry/PDM/uv project — it bypasses the lockfile. + +## Skip Coverage Tools + +Do not configure or run coverage tools (coverage.py, pytest-cov). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/ruby.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/ruby.md new file mode 100644 index 0000000..6307f68 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/ruby.md @@ -0,0 +1,191 @@ +# Ruby Extension + +Language-specific guidance for Ruby test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `spec/**/*_spec.rb` (RSpec) or `test/**/*_test.rb` (Minitest) and copy their style (matchers, helpers, factories, contexts) +2. **`Gemfile` / `Gemfile.lock`** — Ruby version, test framework, supporting gems (`rspec`, `minitest`, `factory_bot`, `webmock`, `vcr`, `rails`) +3. **`.ruby-version`** / `.tool-versions` — pinned Ruby version +4. **Test helpers** — `spec/spec_helper.rb`, `spec/rails_helper.rb`, `test/test_helper.rb` — these dictate the load path, requires, and global config +5. **Rake tasks** — `Rakefile` may define a `default` task that runs the full test suite + +Use the framework the repo already uses. Do not introduce RSpec into a Minitest project (or vice versa). + +## Toolchain Detection + +| Indicator | Manager | Run prefix | +|-----------|---------|------------| +| `Gemfile.lock` | Bundler | `bundle exec ` | +| `.ruby-version` + `rbenv` | rbenv | combine with `bundle exec` | +| `mise.toml` / `asdf` `.tool-versions` | mise/asdf | the wrapper handles version selection; still use `bundle exec` | +| Plain Ruby, no Bundler | system Ruby | `ruby ` (rare in real projects) | + +Always run inside `bundle exec` if a `Gemfile.lock` is present — otherwise you may pick up a system gem version that disagrees with the lockfile. + +## Build Commands + +Ruby is interpreted — there is no compile step. The closest validations: + +| Scope | Command | +|-------|---------| +| Syntax check | `ruby -c path/to/file.rb` | +| Lint (RuboCop) | `bundle exec rubocop path/to/file.rb` | +| Type check (Sorbet) | `bundle exec srb tc` (only if `sorbet/` dir exists) | +| Type check (RBS/Steep) | `bundle exec steep check` | + +For Rails: load all classes once with `bundle exec rails zeitwerk:check` to catch missing constants before running tests. + +## Test Commands + +### RSpec + +| Scope | Command | +|-------|---------| +| All specs | `bundle exec rspec` | +| Single file | `bundle exec rspec spec/models/widget_spec.rb` | +| Single line | `bundle exec rspec spec/models/widget_spec.rb:42` | +| By name | `bundle exec rspec -e "creates a widget"` | +| Tagged | `bundle exec rspec --tag focus` | +| Fail fast | `bundle exec rspec --fail-fast` | +| Documentation format | `bundle exec rspec --format documentation` | + +### Minitest + +| Scope | Command | +|-------|---------| +| All tests | `bundle exec rake test` (Rails) or `bundle exec ruby -Ilib -Itest -e 'Dir.glob("./test/**/*_test.rb").each { |f| require f }'` | +| Single file | `bundle exec ruby -Itest test/models/widget_test.rb` | +| Single test | `bundle exec ruby -Itest test/models/widget_test.rb -n test_creates_widget` | +| By name pattern | `... -n /pattern/` | + +### Rails (any framework) + +| Scope | Command | +|-------|---------| +| Default suite | `bin/rails test` (Minitest) or `bundle exec rspec` | +| Single Rails test file | `bin/rails test test/models/widget_test.rb:42` | +| System tests | `bin/rails test:system` | + +Always prefer the wrapper script (`bin/rails`, `bin/rspec`) when present — they enforce the project's loader/setup. + +## Lint Command + +- `bundle exec rubocop` — autocorrect with `bundle exec rubocop -A` (only if existing tests already conform; do not autocorrect unrelated files) +- `bundle exec standardrb --fix` if `standard` is in the Gemfile +- Some Rails projects add `rubocop-rails`, `rubocop-rspec`, `rubocop-performance` — they enforce extra rules + +## Project Layout and Loading + +| Layout | Test placement | +|--------|----------------| +| Plain gem (RSpec) | `spec/` mirrors `lib/` (e.g. `lib/foo/bar.rb` → `spec/foo/bar_spec.rb`) | +| Plain gem (Minitest) | `test/` mirrors `lib/` (e.g. `test/foo/bar_test.rb`) | +| Rails (RSpec) | `spec/models`, `spec/controllers`, `spec/requests`, `spec/system`, etc. | +| Rails (Minitest) | `test/models`, `test/controllers`, `test/integration`, `test/system` | + +**Loading source code:** + +- RSpec: `spec/spec_helper.rb` typically does `require 'my_gem'` or sets `$LOAD_PATH`. Match its pattern in new specs by `require 'spec_helper'` (or `require 'rails_helper'` in Rails) +- Minitest: each `_test.rb` typically `require 'test_helper'` +- Rails uses Zeitwerk autoloading — do **not** add `require_relative '../../app/models/widget'`; just `require 'rails_helper'` and reference the constant + +## Test File Naming + +| Framework | File suffix | Class/example | +|-----------|-------------|---------------| +| RSpec | `_spec.rb` | `RSpec.describe Widget do ... end`, `it "..." do ... end` | +| Minitest (classic) | `_test.rb` | `class WidgetTest < Minitest::Test`, methods `def test_...` | +| Minitest (spec) | `_test.rb` | `describe Widget do ... it "..." do ... end end` | +| Rails Minitest | `_test.rb` | `class WidgetTest < ActiveSupport::TestCase` | + +## RSpec Template + +```ruby +require 'spec_helper' +require 'calculator' + +RSpec.describe Calculator do + subject(:calculator) { described_class.new } + + describe '#add' do + it 'returns the sum of two positive numbers' do + expect(calculator.add(2, 3)).to eq(5) + end + + context 'with negative numbers' do + it 'returns the correct sum' do + expect(calculator.add(-1, 1)).to eq(0) + end + end + + it 'raises when given non-numeric input' do + expect { calculator.add('a', 1) }.to raise_error(TypeError) + end + end +end +``` + +## Common Errors + +| Error | Fix | +|-------|-----| +| `LoadError: cannot load such file -- foo` | Missing `require` or load path; check `spec_helper.rb` for the established pattern instead of patching `$LOAD_PATH` ad hoc | +| `NameError: uninitialized constant X` | Constant isn't loaded — in Rails, ensure you require `rails_helper`; in plain Ruby, add the appropriate `require` | +| `ArgumentError: wrong number of arguments (given X, expected Y)` | Read the method signature; pass keyword vs positional args correctly | +| `NoMethodError: undefined method 'foo' for nil:NilClass` | Test setup left a value `nil`; check `let`/`before` ordering and factory data | +| `Failure/Error: ... received :foo with unexpected arguments` (RSpec) | Tighten the matcher: `with(hash_including(...))` or relax to `with(any_args)` deliberately | +| `expected #<...> to receive :foo (1 time) but received it 0 times` | Either the code path didn't call the stub, or you stubbed the wrong receiver | +| `DEPRECATION WARNING` (Rails) | Address the deprecation rather than silencing it; tests that warn today break tomorrow | +| `ActiveRecord::PendingMigrationError` | Run `bin/rails db:migrate RAILS_ENV=test` before tests | +| `Mysql2::Error / PG::ConnectionBad` in CI | Tests need a database — check `config/database.yml` and CI service containers | +| `Capybara::ElementNotFound` (system tests) | Use `find` with explicit waits; do not add `sleep` | + +## Mocking Rules (RSpec) + +- Use `instance_double(Klass)` and `class_double(Klass)` — they verify that the method actually exists, unlike `double` +- `allow(obj).to receive(:method).and_return(value)` for stubs; `expect(obj).to receive(:method)` for interaction expectations +- Prefer `instance_double` over plain `double`; prefer dependency injection over `allow_any_instance_of` +- Use `let` for memoized helpers; use `let!` only when the side effect must run before each example +- Avoid global state mutation in tests — wrap in `around` blocks or use `ClimateControl` for env vars +- For HTTP, use `webmock` (`stub_request(:get, ...)`) or `vcr` cassettes if the project already uses them +- If a test needs more than 3 mocks, flag it as a design smell + +## Mocking Rules (Minitest) + +- Use `Minitest::Mock` for simple cases: `mock = Minitest::Mock.new; mock.expect(:method, return_value, [arg])` +- For richer mocking, projects commonly add `mocha`: `obj.expects(:method).returns(value)` (in `test_helper.rb`: `require 'mocha/minitest'`) +- Always verify mocks at end of test (`mock.verify` for `Minitest::Mock`); Mocha verifies automatically + +## Rails Specifics + +- Use the **smallest** spec type that covers the behavior: model spec for pure logic, request spec for HTTP, system spec only when JS/UI matters +- `rails-controller-testing` gem must be present for `assigns(:foo)` and `assert_template` +- `ActiveJob::TestHelper` and `ActiveSupport::Testing::TimeHelpers` (`travel_to`) come with Rails — use them instead of `Timecop` if Rails ≥ 5 +- Use fixtures only if the project already uses them; `factory_bot` is more common in modern Rails apps +- Database transactions wrap each test by default — for system tests with browser drivers, use `DatabaseCleaner` strategies the project already configures + +## Dependency Installation (Last Resort) + +Only add gems after investigation confirms they are missing. Edit `Gemfile`: + +```ruby +group :test do + gem 'rspec' + gem 'webmock' +end +``` + +Then run: + +``` +bundle install +``` + +Never `gem install` outside Bundler — it bypasses the lockfile and changes the global Ruby environment. + +## Skip Coverage Tools + +Do not configure or run coverage tools (SimpleCov). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/rust.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/rust.md new file mode 100644 index 0000000..b383435 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/rust.md @@ -0,0 +1,180 @@ +# Rust Extension + +Language-specific guidance for Rust test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — look at `#[cfg(test)] mod tests` blocks inside `src/`, integration tests in `tests/`, doc tests in source comments, and any `examples/` that double as smoke tests +2. **`Cargo.toml`** — workspace layout (`[workspace]`), edition, `dev-dependencies`, feature flags, `[[bench]]` / `[[test]]` declarations +3. **`Cargo.lock`** — if checked in, you must not break it without intent +4. **Toolchain** — `rust-toolchain.toml` pins the channel (stable / nightly / specific version) +5. **`build.rs`** — custom build scripts may set `cfg` flags or generate code that tests rely on + +Match the repo's existing conventions — assertion macros, mock approach, feature-gating — exactly. Do not introduce `tokio::test` if the repo uses `async-std`, etc. + +## Toolchain Detection + +| Indicator | Meaning | +|-----------|---------| +| `rust-toolchain.toml` with `channel = "..."` | Use rustup to install/select that channel — `rustup show active-toolchain` | +| `rust-version = "1.x"` in `Cargo.toml` | Minimum supported Rust version (MSRV); do not use newer language features | +| `[workspace]` in root `Cargo.toml` | Multi-crate workspace; commands accept `-p ` to target one member | +| `nightly` channel | Tests may use `#![feature(...)]` flags; do not remove them | + +## Build Commands + +| Scope | Command | +|-------|---------| +| Type-check fast | `cargo check` | +| Type-check whole workspace | `cargo check --workspace --all-targets` | +| Build (debug) | `cargo build` | +| Build with all features | `cargo build --all-features` | +| Build a single crate | `cargo build -p crate-name` | +| Build tests without running | `cargo test --no-run` | + +`cargo check` is far faster than `cargo build` and catches almost the same errors. Prefer it during the fix loop; use `cargo build --tests` (or `cargo test --no-run`) before declaring tests compilable. + +## Test Commands + +| Scope | Command | +|-------|---------| +| All tests | `cargo test` | +| Workspace | `cargo test --workspace` | +| Single crate | `cargo test -p crate-name` | +| Filter by name | `cargo test substring_of_test_name` | +| Exact name | `cargo test -- --exact path::to::test_fn` | +| Single integration file | `cargo test --test file_stem` (no `.rs`) | +| Doc tests only | `cargo test --doc` | +| Show stdout | `cargo test -- --nocapture` | +| Single-threaded | `cargo test -- --test-threads=1` | +| Ignored tests | `cargo test -- --ignored` | +| With features | `cargo test --features "feat1 feat2"` | +| All features | `cargo test --all-features` | + +- Arguments before `--` are for cargo; arguments after `--` go to the test binary +- `cargo test foo` runs every test with `foo` in its full path (`module::tests::foo_does_a_thing`) — to avoid surprise matches use `--exact` +- `cargo nextest run` is significantly faster if the repo already uses it (`Cargo.toml` `[profile.nextest...]` or `.config/nextest.toml`) — match the repo's choice + +## Lint Command + +Use the repo's lint script first. Otherwise: + +- `cargo fmt --all -- --check` (CI), `cargo fmt` (apply) +- `cargo clippy --all-targets --all-features -- -D warnings` +- If `clippy.toml` / `rustfmt.toml` exists, the project has opinions — never override them in your tests + +## Project Layout + +``` +my_crate/ +├── Cargo.toml +├── src/ +│ ├── lib.rs # library crate root +│ ├── main.rs # binary crate root (mutually OK with lib.rs) +│ └── module.rs # private/public module +├── tests/ # integration tests — each .rs is a separate crate +│ └── widget.rs +├── benches/ # cargo bench targets +└── examples/ # cargo run --example name +``` + +| Test type | Where | Sees | +|-----------|-------|------| +| Unit test | `#[cfg(test)] mod tests` inside the source file | Private items in the surrounding module | +| Integration test | `tests/.rs` | Only the public API of the crate | +| Doc test | `///` doctests in source comments | Only the public API; runs via `cargo test --doc` | + +- **Unit tests** at the bottom of `module.rs`: + + ```rust + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn name_scenario_expected() { + // ... + } + } + ``` + +- **Integration tests** import the crate by name: `use my_crate::PublicType;` +- Helpers shared between integration tests must live in `tests/common/mod.rs` (the `mod.rs` form prevents cargo from treating them as a top-level test crate) + +## Test Function Patterns + +| Kind | Attribute | +|------|-----------| +| Sync test | `#[test]` | +| Should panic | `#[test] #[should_panic(expected = "message substring")]` | +| Ignored (long/manual) | `#[test] #[ignore = "reason"]` | +| Async test (Tokio) | `#[tokio::test]` (or `#[tokio::test(flavor = "multi_thread")]`) | +| Async test (async-std) | `#[async_std::test]` | +| Returning `Result` | `fn name() -> Result<(), Box>` — use `?` instead of `.unwrap()` | + +Pick the async harness the repo already uses. Do not mix `tokio` and `async-std` in tests. + +## Common Errors + +| Error | Fix | +|-------|-----| +| `cannot find type X in this scope` | Add `use crate::module::X;` or `use super::*;` inside the test module | +| `function or associated item not found in 'X'` | Verify the method exists on the exact type; check trait imports (e.g. `use std::io::Read`) | +| `the trait bound 'X: Y' is not satisfied` | Either implement the trait, add a `where` bound, or change the test to use a type that already implements it | +| `borrow of moved value` | Add `.clone()`, borrow with `&`, or restructure ownership — do not use `mem::transmute` to dodge it | +| `cannot borrow as mutable` | Make the binding `let mut x` or restructure to avoid simultaneous mutable + immutable borrows | +| `lifetime may not live long enough` | Add explicit lifetime annotations or use owned types (`String` instead of `&str`) in the test | +| `mismatched types` between `i32` and `usize` | Use `as` casts deliberately or change the literal type with a suffix (`5usize`, `5u32`) | +| `unresolved import 'crate::...'` in `tests/foo.rs` | Integration tests must import via the **crate name** (as listed in `Cargo.toml`), not `crate::` | +| `error: no test target found` for `cargo test --test foo` | The file must live directly in `tests/`, not `tests/subdir/foo.rs` (subdirs are treated as helpers) | +| `attempt to subtract with overflow` (debug) | Underflow on unsigned types; use `checked_sub`/`saturating_sub` or compare before subtracting | +| Doctest fails to compile | Use a leading "# " on hidden setup lines; mark code blocks `ignore`/`no_run`/`should_panic` if needed | +| `the following imports are unused` (warning treated as error) | Remove unused `use` statements; do not silence with `#[allow(unused_imports)]` | + +## Mocking Rules + +Rust has no single dominant mocking framework. Match the repo: + +- **Trait + struct fakes** (most idiomatic): define a trait, pass `Arc` or generic `T: Trait`, implement a fake struct in tests +- **`mockall`** crate: `#[automock]` on a trait generates `MockTrait` for use in tests +- **`mockito`** / **`wiremock`**: HTTP server mocks for client tests +- **`tempfile`**: scoped temp directories that auto-clean (`tempfile::tempdir()`) + +Avoid `unsafe` patches to "mock" free functions. Refactor to inject a trait instead. If a test needs more than 3 mocks, flag it as a design smell. + +## Features and `cfg` + +- Tests behind a feature flag run only when that feature is enabled — use `#[cfg(feature = "foo")]` on the `mod tests` or individual `#[test]` functions +- `--all-features` exercises everything but may pull conflicting features in some workspaces; check `cargo test --all-features` is part of CI before relying on it +- Use `#[cfg(test)]` to gate test-only helpers in production source files — not `#[cfg(feature = "test")]` + +## Concurrency, IO, and `unsafe` + +- Tests run in parallel by default. If your tests share global state (env vars, current dir, statics), serialize them with the `serial_test` crate (if present) or move state into the test +- Never write to `/tmp` or the repo dir directly — use `tempfile::tempdir()` so cleanup is automatic +- Tests in `unsafe` code should also run under Miri (`cargo +nightly miri test`) if the repo's CI does + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing: + +```toml +[dev-dependencies] +mockall = "0.12" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +``` + +Or via cargo: + +``` +cargo add --dev mockall +cargo add --dev tokio --features macros,rt-multi-thread +``` + +Match the major version of any tokio/serde/etc. already pinned by the workspace. + +## Skip Coverage Tools + +Do not configure or run coverage tools (`cargo tarpaulin`, `cargo llvm-cov`, `grcov`). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/swift.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/swift.md new file mode 100644 index 0000000..e4bf7b9 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/swift.md @@ -0,0 +1,227 @@ +# Swift Extension + +Language-specific guidance for Swift test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find files in `Tests/` (SPM) or `*Tests/` groups (Xcode) and copy their style. Distinguish **XCTest** (`import XCTest`, classes inheriting `XCTestCase`) from **Swift Testing** (`import Testing`, free functions tagged `@Test`) +2. **Project file** — `Package.swift` (SPM), `*.xcodeproj`, `*.xcworkspace`, or `Project.swift` (Tuist) +3. **Swift toolchain** — `.swift-version`, `swift-tools-version` line in `Package.swift`, `IPHONEOS_DEPLOYMENT_TARGET` and `SWIFT_VERSION` build settings in Xcode +4. **CI scripts** — `.github/workflows/*.yml`, `Fastfile`, `Makefile` — these reveal the canonical build/test invocation + +Use the testing framework the repo already uses. Both XCTest and Swift Testing can coexist in one target — match what the file you're adding tests next to uses. + +## Project Type Detection + +| Indicator | Project type | Build tool | +|-----------|--------------|------------| +| `Package.swift` only | Swift Package Manager | `swift build` / `swift test` | +| `*.xcodeproj` or `*.xcworkspace` | Xcode project (often app/iOS) | `xcodebuild` | +| Both | SPM library + Xcode app shell | Use SPM for library targets, Xcode for app targets | +| `Project.swift` (Tuist) | Tuist-generated Xcode project | Run `tuist generate` first, then xcodebuild | +| `project.yml` (XcodeGen) | XcodeGen-generated project | Run `xcodegen generate` first | + +If both an `.xcodeproj` and `.xcworkspace` exist (e.g. CocoaPods), **always pass `-workspace` not `-project`** to xcodebuild. + +## Build Commands + +### Swift Package Manager + +| Scope | Command | +|-------|---------| +| Build all | `swift build` | +| Build a target | `swift build --target MyLibrary` | +| Build for release | `swift build -c release` | + +### Xcode (`xcodebuild`) + +``` +xcodebuild build \ + -workspace MyApp.xcworkspace \ + -scheme MyAppScheme \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -configuration Debug +``` + +- Always specify `-destination` for iOS/tvOS/watchOS — the default may not exist on the build machine +- Use `-quiet` to suppress xcodebuild's chatty output, and pipe to `xcbeautify`/`xcpretty` if installed +- For deterministic CI builds add `-derivedDataPath ./DerivedData` + +## Test Commands + +### Swift Package Manager + +| Scope | Command | +|-------|---------| +| All tests | `swift test` | +| Filter by test name (XCTest) | `swift test --filter MyClassTests/testFooBar` | +| Filter by test name (Swift Testing) | `swift test --filter MyTestSuite.fooBar` | +| Parallel | `swift test --parallel` | +| Single platform | `swift test --triple x86_64-apple-macosx` (rare; usually skip) | + +### Xcode + +``` +xcodebuild test \ + -workspace MyApp.xcworkspace \ + -scheme MyAppScheme \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -only-testing:MyAppTests/MyClassTests/testFooBar +``` + +- `-only-testing:` and `-skip-testing:` accept `Bundle/Class/Method` paths and may be repeated +- `xcodebuild test-without-building` skips compilation if you've already built +- For Swift Testing in Xcode 16+, use the same `-only-testing:` syntax — the runner handles both frameworks + +## Lint Command + +Use the repo's lint tooling first: + +- `swiftlint lint --quiet` (autocorrect: `swiftlint --fix`) when `.swiftlint.yml` is present +- `swiftformat .` when `.swiftformat` is present +- Some projects gate format on a build phase — running `xcodebuild` may already invoke it + +## Project Layout + +### SPM + +``` +Package.swift +Sources/ +└── MyLibrary/ + ├── Foo.swift + └── Bar.swift +Tests/ +└── MyLibraryTests/ + └── FooTests.swift +``` + +- Test target name conventionally is `Tests` and lives in `Tests/Tests/` +- Test target must list its production target as a dependency in `Package.swift`: + + ```swift + .testTarget( + name: "MyLibraryTests", + dependencies: ["MyLibrary"]), + ``` + +### Xcode + +- Tests live in a separate target (e.g. `MyAppTests`) added to the scheme's "Test" action +- The test target's "Host Application" determines whether tests run on the simulator with the app loaded (unit tests) or as a UI test runner + +## Imports + +- XCTest: `import XCTest` plus `@testable import MyLibrary` to access `internal` symbols +- Swift Testing: `import Testing` plus `@testable import MyLibrary` +- `@testable` works only when the production target is built with `-enable-testing` (the SPM test target and Xcode "Debug" config do this by default) +- Never mark production code `public` solely to make it visible to tests — use `@testable import` instead + +## Test File Templates + +### Swift Testing (Xcode 16 / Swift 6) + +```swift +import Testing +@testable import MyLibrary + +@Suite("Calculator") +struct CalculatorTests { + @Test("add returns the sum of two integers") + func addReturnsSum() { + let calc = Calculator() + #expect(calc.add(2, 3) == 5) + } + + @Test("add throws on overflow", arguments: [ + (Int.max, 1), + (Int.min, -1), + ]) + func addThrowsOnOverflow(a: Int, b: Int) { + #expect(throws: ArithmeticError.self) { + try Calculator().add(a, b) + } + } +} +``` + +### XCTest + +```swift +import XCTest +@testable import MyLibrary + +final class CalculatorTests: XCTestCase { + func testAddReturnsSum() { + let calc = Calculator() + XCTAssertEqual(calc.add(2, 3), 5) + } + + func testAddThrowsOnOverflow() { + XCTAssertThrowsError(try Calculator().add(.max, 1)) { error in + XCTAssertEqual(error as? ArithmeticError, .overflow) + } + } +} +``` + +- XCTest requires test methods to start with `test` and take no arguments +- Mark XCTest classes `final` to silence warnings and prevent unintended subclassing +- Use `XCTUnwrap` instead of force-unwrapping (`!`) inside tests so the failure is reported rather than crashing the runner + +## Async, Throws, and Concurrency + +- Test methods may be `async` and/or `throws` in both frameworks +- For asynchronous expectations under XCTest, use `XCTestExpectation` + `wait(for:timeout:)` only when you cannot refactor to `async` +- For Swift Testing, use `await confirmation { ... }` to assert that a callback fires +- Cancel tasks deliberately with `Task.cancel()` instead of relying on test timeout + +## Common Errors + +| Error | Fix | +|-------|-----| +| `cannot find 'X' in scope` from a test | Add `@testable import MyLibrary` (and ensure the test target depends on it) | +| `module 'MyLibrary' was not compiled for testing` | Build the production target with `-enable-testing`; SPM test targets do this automatically — Xcode Debug configs need "Enable Testability" = YES | +| `failed to launch test runner` (Xcode) | Simulator destination may be invalid; list with `xcrun simctl list devices` and pick an existing one | +| `No such module 'XCTest'` outside a test target | XCTest is only available in test targets — do not import it from production code | +| `Static method 'expect(_:_:sourceLocation:)' is unavailable` / `No such module 'Testing'` | Swift Testing requires Swift 6 / Xcode 16+. On older toolchains, fall back to XCTest | +| `Symbol not found: _OBJC_CLASS_$_...` | Linker missing a framework; add it to the test target's "Link Binary With Libraries" | +| `signal SIGABRT` in tests | Often a force-unwrap on `nil`; replace `!` with `XCTUnwrap` to localize the failure | +| `MainActor-isolated property cannot be referenced from a non-isolated context` | Mark the test method `@MainActor` or move setup into a `MainActor` task | +| `Sandbox: ... deny file-write-create` | Use `FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)` instead of writing to fixed paths | +| Test discovery shows zero tests on Linux | XCTest on Linux needs `XCTMain([testCase(MyTests.allTests), ...])` in `Tests/LinuxMain.swift` (legacy SwiftPM only); for Swift 5.4+ this is auto-generated | + +## Mocking Rules + +Swift has no Mockito-equivalent — favor protocol-oriented design: + +- Define a **protocol** for the dependency, pass it via initializer, and implement a fake/stub struct in the test target +- For URL/HTTP, use `URLProtocol` subclasses to intercept `URLSession` requests, or use `MockingbirdSwift` / `Cuckoo` if the repo already adopts them +- For dates/clocks, inject a `Clock` (`ContinuousClock`, `SuspendingClock`, or a custom `Clock`-conforming type) — do not call `Date()` directly in business logic +- Avoid `swizzling` and runtime hacks — they break under Swift's optimizer + +If a test needs more than 3 mocks, flag it as a design smell. + +## Cross-Platform Considerations + +- Swift on Linux supports XCTest but **not** all of Foundation — guard with `#if canImport(Darwin)` or `#if os(macOS)` only when necessary +- Use `String(decoding:as:)` rather than `String(contentsOf:encoding:)` for cross-platform reads +- Be careful with `Bundle.main` in tests — on macOS unit tests it points to `xctest`, not your bundle; use `Bundle(for: type(of: self))` (XCTest) or a resource-bundle helper + +## Dependency Installation (Last Resort) + +Only add dependencies after investigation confirms they are missing. + +`Package.swift`: + +```swift +.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), +``` + +Then add to the test target's `dependencies:`. For CocoaPods/Carthage, edit `Podfile`/`Cartfile` and run `pod install` / `carthage update --use-xcframeworks`. + +## Skip Coverage Tools + +Do not configure or run coverage tools (`-enableCodeCoverage YES`, `xccov`, `slather`). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/typescript.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/typescript.md new file mode 100644 index 0000000..de26a70 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/code-testing-extensions/extensions/typescript.md @@ -0,0 +1,136 @@ +# TypeScript Extension + +Language-specific guidance for TypeScript (and JavaScript) test generation. + +## Rule #1: Investigate the Repo First + +Before writing any test or running any command, read: + +1. **Existing tests** — find `*.test.ts` / `*.spec.ts` files and copy their style (imports, describe/it vs test, assertion patterns, mock approach) +2. **`package.json`** — `scripts.test`, `devDependencies`, `type` field +3. **Config files** — `tsconfig.json`, `jest.config.*`, `vitest.config.*`, `eslint.config.*` + +Use the repo's existing test runner and conventions — do not switch frameworks. If multiple runners are configured, follow whichever `scripts.test` invokes. Only introduce a framework if the repo has no tests at all. + +## Package Manager Detection + +Detect the package manager from lockfiles and use it consistently for **all** commands: + +| Indicator | Manager | Run script | Execute binary | +|-----------|---------|------------|----------------| +| `pnpm-lock.yaml` | pnpm | `pnpm test` | `pnpm exec ` | +| `yarn.lock` | Yarn | `yarn test` | `yarn ` | +| `bun.lockb` / `bun.lock` | Bun | `bun test` | `bunx ` | +| `package-lock.json` or none | npm | `npm test` | `npx ` | + +Use `` below as shorthand for the detected exec command. + +## Build Commands + +| Scope | Command | +|-------|---------| +| Type check | ` tsc --noEmit` or the repo's `typecheck` script | +| Build (if configured) | The repo's `build` script | + +Many projects don't need an explicit build step — the test runner handles transpilation. + +## Test Commands + +Detect the runner from `devDependencies` and `scripts.test`. Always prefer the repo's test script first. + +| Runner | Run once | Filter by file | Filter by name | +|--------|----------|----------------|----------------| +| **Jest** | ` jest` | ` jest path/to/file` | ` jest -t "name"` | +| **Vitest** | ` vitest run` | ` vitest run path/to/file` | ` vitest run -t "name"` | +| **Mocha** | ` mocha` | (use config or positional args) | ` mocha --grep "name"` | + +- **Always use `vitest run`** (not bare `vitest`) — bare `vitest` starts watch mode +- **Never use `--watch`** — the agent must not start interactive/watch mode +- For Jest: `--bail` to stop on first failure, `--verbose` for detail +- Mocha `--grep` filters by **test name**, not file path + +## Lint Command + +Use the repo's lint script first. Otherwise detect from `devDependencies` and config: + +- `eslint.config.*` or `.eslintrc.*` → ` eslint --fix path/to/file.ts` +- `prettier` → ` prettier --write path/to/file.ts` +- `biome.json` → ` biome check --write path/to/file.ts` + +## Project Layout and Imports + +| Layout | Import Style | +|--------|-------------| +| Colocated (`src/module.test.ts`) | `import { X } from './module'` | +| `__tests__/` dir | `import { X } from '../module'` | +| Top-level `tests/` | `import { X } from '../src/module'` | + +- **Match existing test imports** — copy path style from neighboring tests +- If `tsconfig.json` has `paths` aliases (e.g., `@/`), use them in tests too +- For monorepos: import from the package name, not relative cross-package paths +- For monorepo workspaces (Nx, Turborepo, Lerna): run tests via the workspace tool (`nx test `, `turbo test`), not from a random package directory + +## Test File Naming + +- Match existing convention — check for `.test.ts` vs `.spec.ts` +- Jest/Vitest default: `*.test.ts`, `*.spec.ts`, or files inside `__tests__/` +- Place test files to mirror the existing project pattern + +## Common Errors + +| Error | Fix | +|-------|-----| +| `Cannot find module 'X'` | Check existing imports for correct paths; verify `tsconfig.json` `paths`; check `moduleNameMapper` (Jest) or `resolve.alias` (Vitest) | +| `TS2305: has no exported member` | Verify the exact export name from the source file | +| `TS2345: type not assignable` | Match the expected type; use type assertion only for mock objects | +| `SyntaxError: Unexpected token` / `Jest encountered an unexpected token` | Verify TS transform config (`ts-jest`, `@swc/jest`, or Vitest handles natively) | +| `ReferenceError: describe is not defined` | Vitest: import from `vitest` or set `globals: true` in config; Jest: ensure tests run under Jest not bare `node` | +| `Cannot use import statement outside a module` / `ERR_REQUIRE_ESM` | ESM/CJS mismatch — align runner config with the project's module system (see ESM section); do **not** blindly set `"type": "module"` | +| `ReferenceError: document is not defined` | Set test environment: `testEnvironment: 'jsdom'` (Jest) or `environment: 'jsdom'` (Vitest) | +| `jest.mock() ... out-of-scope variables` | Keep `jest.mock()` at top level; don't reference variables declared after the mock call (Jest hoists mocks) | +| `Cannot find module '@/...'` | Mirror the project's alias config in the test runner's module resolution | +| `Warning: not wrapped in act(...)` | Await async UI updates using the repo's existing pattern (`waitFor`, `act`) | + +## ESM vs CommonJS + +Check these signals to determine the project's module system: + +- `"type": "module"` in `package.json` → ESM +- `"module": "ESNext"` or `"NodeNext"` in `tsconfig.json` → ESM output (but not sufficient alone) +- `.mjs`/`.mts` extensions → ESM files + +If the test runner fails with ESM errors, align the runner's config with the project's module system. **Do not change `package.json` `type` field** — align the test runner to match whatever the project uses: + +- **Jest**: `--experimental-vm-modules` + `ts-jest` with `useESM: true`, or `@swc/jest` +- **Vitest**: handles ESM natively +- **Mocha**: `--loader ts-node/esm` + +## Mocking Rules + +- Prefer dependency injection over module mocking +- Use typed mocks: `jest.Mocked`, `vi.mocked(obj)`, or `Partial` with `as T` +- Jest: `jest.mock()` is hoisted — keep at top level, don't close over local variables +- Vitest: `vi.mock()` follows the same hoisting rules +- If a test needs more than 3–4 mocks, flag it as a design smell +- Mock reset: rely on `clearMocks`/`restoreMocks` config if present; otherwise reset in `beforeEach` + +## Framework-Specific Notes + +- **React/Preact**: use `@testing-library/react`, wrap with necessary providers (router, query client, theme) matching existing test setup +- **Express/Koa**: use `supertest` for HTTP testing if the repo already uses it +- **NestJS**: build testing module with `Test.createTestingModule` — don't instantiate controllers directly + +## Dependency Installation (Last Resort) + +Only install packages after investigation confirms they are missing. Use the detected package manager: + +``` + add --save-dev jest ts-jest @types/jest + add --save-dev vitest +``` + +Never install test infrastructure that conflicts with what the repo already uses. + +## Skip Coverage Tools + +Do not configure or run coverage tools (istanbul, c8, `vitest --coverage`). Coverage is measured separately by the evaluation harness. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/SKILL.md index 1bfbff3..128f2bd 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/SKILL.md @@ -1,20 +1,17 @@ --- name: coverage-analysis description: > - Automated, project-wide code coverage and CRAP (Change Risk Anti-Patterns) - score analysis for .NET projects with existing unit tests. Auto-detects - solution structure, runs coverage collection via `dotnet test` (supports both - Microsoft.Testing.Extensions.CodeCoverage and Coverlet), generates reports via - ReportGenerator, calculates CRAP scores per method, and surfaces risk - hotspots — complex code with low test coverage that is dangerous to modify. - Use when the user wants project-wide coverage analysis with risk - prioritization, coverage gap identification, CRAP score computation - across an entire solution, or to diagnose why coverage is stuck or - plateaued and identify what methods are blocking improvement. - DO NOT USE FOR: targeted single-method CRAP analysis (use crap-score skill), - writing tests, running tests without coverage collection, applying test - filters, producing TRX reports, or troubleshooting test execution (use - run-tests for all of these). + Project-wide code coverage and CRAP (Change Risk Anti-Patterns) score + analysis for .NET projects. Calculates CRAP scores per method and surfaces + risk hotspots — complex code with low coverage that is dangerous to modify. + Use to diagnose why coverage is stuck or plateaued, identify what methods + block improvement, or get project-wide coverage analysis with risk ranking. + USE FOR: coverage stuck, coverage plateau, can't increase coverage, what's + blocking coverage, coverage gap, CRAP scores, risk hotspots, where to add + tests, coverage analysis, coverage report. + DO NOT USE FOR: targeted single-method CRAP analysis (use crap-score), + writing tests, running tests without coverage, or troubleshooting test + execution (use run-tests). license: MIT --- @@ -55,8 +52,9 @@ Use this skill when the user mentions test coverage, coverage gaps, code risk, C ### Prerequisites - .NET SDK installed (`dotnet` on PATH) -- At least one test project referencing the production code (xUnit, NUnit, or MSTest) -- Internet access for `dotnet tool install` (ReportGenerator) on first run, or ReportGenerator already installed globally +- At least one test project referencing the production code (xUnit, NUnit, or MSTest) — only required for the from-scratch path; not needed when the user supplies an existing Cobertura XML +- **Optional, only for the from-scratch path:** internet/NuGet access for `dotnet add package coverlet.collector` (or `Microsoft.Testing.Extensions.CodeCoverage`) when a test project has no coverage provider yet. Skip when the user supplies an existing Cobertura XML. +- **Optional, only for Phase 5:** internet access for `dotnet tool install` (ReportGenerator). Core CRAP/coverage analysis works from Cobertura XML alone — ReportGenerator only adds HTML/CSV reports as an optional post-summary extra. The skill auto-detects coverage provider state per test project and selects the least-invasive execution strategy: @@ -68,9 +66,13 @@ No pre-existing runsettings files or manually installed tools required. ## Workflow -If the user provides a path to existing Cobertura XML (or coverage data is already present in `TestResults/`), skip Steps 3–4 (test execution and provider detection) but **still run Steps 5–6** (ReportGenerator and CRAP score computation). The Risk Hotspots table and CRAP scores are mandatory in every output — they are the skill's core value-add over raw coverage numbers. +> **MANDATORY: deliver the final assistant response with the CRAP/risk-hotspot summary BEFORE any optional work.** As soon as `Compute-CrapScores.ps1` and `Extract-MethodCoverage.ps1` return data, your **next** assistant response must contain the user-facing analysis (CRAP table, blocking methods, recommendations). Do not run ReportGenerator (Phase 5), do not install global tools, and do not start any heavy parallel work before that response is delivered. The user is judged on the final assistant message, not on side-effect files. +> +> If a phase fails, times out, or budget is running low, skip remaining optional work and immediately return a partial summary containing: (1) what was found in the Cobertura XML, (2) any CRAP/risk-hotspot data already extracted, (3) which methods are blocking coverage, and (4) failures encountered. -The workflow runs in four phases. Phases 2 and 3 each contain steps that can run in parallel to reduce total wall-clock time. +If the user provides a path to existing Cobertura XML (or coverage data is already present in `TestResults/`), **skip Phase 2 entirely** (no test execution) **and skip Phase 5 by default** (no ReportGenerator install or HTML report) — go directly from Phase 3 (analysis scripts) to Phase 4 (user-facing summary). Only run Phase 5 if the user explicitly asks for HTML/CSV reports. The Risk Hotspots table and CRAP scores are mandatory in every output — they are the skill's core value-add over raw coverage numbers. + +The workflow runs in five phases. Phases 1–4 are required; Phase 5 (ReportGenerator HTML/CSV reports) is strictly optional and runs **after** the user-facing summary has been delivered. Do not parallelize Phase 5 with earlier phases — the heavy `dotnet tool install` for ReportGenerator can crash the session before Phase 4 completes. ### Phase 1 — Setup (sequential) @@ -131,7 +133,13 @@ Write-Host "TEST_PROJECTS:$($testProjects.Count)" $testProjects | ForEach-Object { Write-Host "TEST_PROJECT:$($_.FullName)" } # Resolve the test output root (where coverage-analysis artifacts will be written) -if ($testProjects.Count -eq 1) { +if ($testProjects.Count -eq 0) { + if ($gitRoot) { + $testOutputRoot = $gitRoot + } else { + $testOutputRoot = $root + } +} elseif ($testProjects.Count -eq 1) { $testOutputRoot = $testProjects[0].DirectoryName } else { # Multiple test projects — find their deepest common parent directory @@ -165,7 +173,8 @@ Write-Host "TEST_OUTPUT_ROOT:$testOutputRoot" - If `ENTRY_TYPE:NotFound` and test projects were found → use the test projects directly as entry points (run `dotnet test` on each test `.csproj`). - If `ENTRY_TYPE:NotFound` and no test projects found → stop: `No .sln or test projects found under . Provide the path to your .NET solution or project.` -- If `TEST_PROJECTS:0` → stop: `No test projects found (expected projects with 'Test' or 'Spec' in the name). Ensure your solution has unit test projects before running coverage analysis.` +- If `TEST_PROJECTS:0` and `EXISTING_COBERTURA_COUNT` > 0 (Step 2b) → continue with existing Cobertura XML analysis (no `dotnet test` run). +- If `TEST_PROJECTS:0` and `EXISTING_COBERTURA_COUNT` == 0 → stop: `No test projects found (expected projects with 'Test' or 'Spec' in the name), and no existing Cobertura XML was provided. Add a test project or provide a Cobertura file path.` #### Step 2: Create the output directory @@ -176,7 +185,40 @@ New-Item -ItemType Directory -Path $coverageDir -Force | Out-Null Write-Host "COVERAGE_DIR:$coverageDir" ``` -#### Step 2b: Recommend ignoring `TestResults/` +This step only manages the `TestResults/coverage-analysis/` subdirectory (skill-owned outputs). It must never delete user-supplied Cobertura files — those live one level up at `TestResults/coverage.cobertura.xml` (or wherever the user pointed). If the user provided a path that *is* `TestResults/coverage-analysis/...`, copy the file aside before this step recreates the directory. + +#### Step 2b: Discover or accept existing Cobertura XML (required for the existing-data path) + +If the user supplied a Cobertura XML path explicitly, use it. Otherwise probe well-known locations and any path the user mentioned: + +```powershell +# 1. Honor a user-supplied path first (highest priority) +$coberturaFiles = @() +if ($userSuppliedCoberturaPath -and (Test-Path $userSuppliedCoberturaPath)) { + $coberturaFiles = @(Get-Item $userSuppliedCoberturaPath) +} + +# 2. Otherwise scan TestResults/ at the repo/test root for any *.cobertura.xml +if ($coberturaFiles.Count -eq 0) { + $searchPaths = @( + (Join-Path $testOutputRoot "TestResults"), + (Join-Path $root "TestResults") + ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique + foreach ($sp in $searchPaths) { + $found = @(Get-ChildItem -Path $sp -Filter "*.cobertura.xml" -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[/\\]coverage-analysis[/\\]raw[/\\]' }) + if ($found.Count -gt 0) { $coberturaFiles = $found; break } + } +} + +Write-Host "EXISTING_COBERTURA_COUNT:$($coberturaFiles.Count)" +$coberturaFiles | ForEach-Object { Write-Host "EXISTING_COBERTURA:$($_.FullName)" } +``` + +- If `EXISTING_COBERTURA_COUNT` > 0 → **skip Phase 2 entirely** and pass these paths to the Phase 3 scripts. +- If `EXISTING_COBERTURA_COUNT` == 0 → run Phase 2 to generate fresh coverage; the file paths to feed Phase 3 will be discovered from `/raw/` after `dotnet test`. + +#### Step 2c: Recommend ignoring `TestResults/` ```powershell $pattern = "**/TestResults/" @@ -198,9 +240,9 @@ if ($gitRoot) { } ``` -### Phase 2 — Data collection (Steps 3 and 4 run in parallel) +### Phase 2 — Test execution (skip when Cobertura XML already exists) -Steps 3 and 4 are independent — start both simultaneously. `dotnet test` is the slowest step, and ReportGenerator setup doesn't need coverage files, so running them concurrently cuts wall time significantly. +Run only when no Cobertura XML is present. If the user already has coverage data, skip directly to Phase 3. #### Step 3: Detect coverage provider and run `dotnet test` with coverage collection @@ -361,7 +403,67 @@ If `COBERTURA_COUNT` is 0: - If `VS_BINARY_COVERAGE` > 0: warn the user — *"Found .coverage files (VS binary format) but no Cobertura XML. These were likely produced by Visual Studio's built-in collector, which outputs a binary format by default. This skill needs Cobertura XML. Re-running with the detected provider configured for Cobertura output."* Then re-run the appropriate `dotnet test` command above (Coverlet or Microsoft CodeCoverage) with Cobertura format. - If no `.coverage` files either: stop and report — *"Coverage files not generated. Ensure `dotnet test` completed successfully and check the build output for errors."* -#### Step 4: Verify or install ReportGenerator (parallel with Step 3) +### Phase 3 — Analysis (sequential) + +Run the two bundled PowerShell scripts. Both are cheap and complete in seconds. **Do not** install or invoke ReportGenerator here — that belongs in optional Phase 5, after the user-facing summary has been delivered. + +#### Step 4: Calculate CRAP scores using the bundled script + +Run `scripts/Compute-CrapScores.ps1` (co-located with this SKILL.md). It reads all Cobertura XML files, applies `CRAP(m) = comp² × (1 − cov)³ + comp` per method, and returns the top-N hotspots as JSON. + +To locate the script: find the directory containing this skill's `SKILL.md` file (the skill loader provides this context), then resolve `scripts/Compute-CrapScores.ps1` relative to it. If the script path cannot be determined, calculate CRAP scores inline using the formula below. + +```powershell +& "/scripts/Compute-CrapScores.ps1" ` + -CoberturaPath @() ` + -CrapThreshold ` + -TopN +``` + +Script outputs: `OVERALL_LINE_COVERAGE:`, `OVERALL_BRANCH_COVERAGE:` (aggregated project-wide rates across all provided Cobertura files), `TOTAL_METHODS:`, `FLAGGED_METHODS:`, `HOTSPOTS:` (top-N sorted by CrapScore descending). The OVERALL_* values are exactly what the Phase 4 summary needs for the "Line Coverage" / "Branch Coverage" rows — no separate XML parsing tool call is required. + +#### Step 5: Extract per-method coverage gaps + +Run `scripts/Extract-MethodCoverage.ps1` to get per-method coverage data for the Coverage Gaps table: + +```powershell +& "/scripts/Extract-MethodCoverage.ps1" ` + -CoberturaPath @() ` + -CoverageThreshold ` + -BranchThreshold ` + -Filter below-threshold +``` + +Script outputs: JSON array of methods below the coverage threshold, sorted by coverage ascending. Use this data to populate the Coverage Gaps by File table in the report. + +### Phase 4 — User-facing summary (MANDATORY — your next assistant response) + +As soon as Phase 3 completes, **your immediately next assistant response must contain the user-facing analysis** — do not interleave any other tool calls before it. This is the response the user (and any judge) sees. Skipping or deferring this in favor of Phase 5 (ReportGenerator) is a hard failure. + +The response must include, at minimum: + +1. Overall line and branch coverage — read directly from the `OVERALL_LINE_COVERAGE:` / `OVERALL_BRANCH_COVERAGE:` lines emitted by `Compute-CrapScores.ps1` (no extra Cobertura parsing required) +2. The Risk Hotspots table built from `Compute-CrapScores.ps1` `HOTSPOTS:` output (CRAP scores, complexity, coverage) +3. Identification of the highest-risk method(s) and what is blocking coverage +4. 1–3 prioritized, specific recommendations (which method to test, expected CRAP/coverage impact) + +Use `references/output-format.md` verbatim for fixed headings, table structures, symbols, and emoji. Use `references/guidelines.md` for prioritization rules and style. + +If Phase 5 has not yet run when you compose this summary, mark the `## 📁 Reports` section's HTML/Text/CSV/GitHub-markdown rows as `Not generated (optional — request HTML reports to enable)`. Only the `coverage-analysis.md` and raw Cobertura paths are guaranteed to exist. + +Attempt to save the same content to `TestResults/coverage-analysis/coverage-analysis.md` before delivering the response (use the editor's create/edit tool — do not shell out). If the file write fails, still deliver the summary and note the file-write failure explicitly. + +### Phase 5 — Optional: ReportGenerator HTML/CSV reports (post-summary) + +Phase 5 is **strictly optional** and runs **only after** Phase 4 has been delivered. Skip Phase 5 entirely when: + +- The user supplied existing Cobertura XML and only asked for analysis (the default for the existing-data path). +- The user is diagnosing a coverage plateau or asking "what's blocking me?" — they want the answer, not a static-site report. +- ReportGenerator is not already installed and you have no clear signal the user wants HTML reports. + +Run Phase 5 only when the user explicitly asks for HTML/CSV reports, or when the project flow requires them (e.g., a CI artifact upload step). + +#### Step 6: Verify or install ReportGenerator (only if running Phase 5) ```powershell $rgAvailable = $false @@ -390,13 +492,9 @@ if ($rgCommand) { Write-Host "RG_AVAILABLE:$rgAvailable" ``` -If installation fails (no internet), keep `RG_AVAILABLE:false` and continue with raw Cobertura XML parsing + script-based analysis in Step 6. Skip HTML/Text/CSV report generation in Step 5 and note this in the output. +If installation fails (no internet), keep `RG_AVAILABLE:false`, leave the existing user-facing summary as the final output, and note that HTML reports were skipped. -### Phase 3 — Analysis (Steps 5 and 6 run in parallel) - -Once Phase 2 completes (coverage files available, ReportGenerator ready), start Steps 5 and 6 simultaneously — both read from the same Cobertura XML and produce independent outputs. - -#### Step 5: Generate reports with ReportGenerator (parallel with Step 6) +#### Step 7: Generate HTML/CSV reports ```powershell $reportsDir = Join-Path "" "reports" @@ -414,60 +512,21 @@ if ($rgAvailable) { } ``` -#### Step 6: Calculate CRAP scores using the bundled script (parallel with Step 5) - -Run `scripts/Compute-CrapScores.ps1` (co-located with this SKILL.md). It reads all Cobertura XML files, applies `CRAP(m) = comp² × (1 − cov)³ + comp` per method, and returns the top-N hotspots as JSON. - -To locate the script: find the directory containing this skill's `SKILL.md` file (the skill loader provides this context), then resolve `scripts/Compute-CrapScores.ps1` relative to it. If the script path cannot be determined, calculate CRAP scores inline using the formula below. - -```powershell -& "/scripts/Compute-CrapScores.ps1" ` - -CoberturaPath @() ` - -CrapThreshold ` - -TopN -``` - -Script outputs: `TOTAL_METHODS:`, `FLAGGED_METHODS:`, `HOTSPOTS:` (top-N sorted by CrapScore descending). - -Also run `scripts/Extract-MethodCoverage.ps1` to get per-method coverage data for the Coverage Gaps table: - -```powershell -& "/scripts/Extract-MethodCoverage.ps1" ` - -CoberturaPath @() ` - -CoverageThreshold ` - -BranchThreshold ` - -Filter below-threshold -``` - -Script outputs: JSON array of methods below the coverage threshold, sorted by coverage ascending. Use this data to populate the Coverage Gaps by File table in the report. - -### Phase 4 — Output (sequential) - -#### Step 7: Build the output report - -Compose the analysis and save it to `TestResults/coverage-analysis/coverage-analysis.md` under the test project directory. Print the full report to the console. - -After saving the file, automatically open `TestResults/coverage-analysis/coverage-analysis.md` in the editor so the user can review it immediately. - -- In editor-hosted environments (VS Code, Visual Studio, or other IDE hosts): open the file in the current host session/editor context after writing it. -- Do not launch a different app instance via hardcoded shell commands (for example `code`, `start`, or platform-specific open commands) unless the host has no native open-file mechanism. -- In CLI or non-editor environments: print the absolute report path and clearly state that the file was generated. - -Do not ask for confirmation before opening the report file. - -Use `references/output-format.md` verbatim for all fixed headings, table structures, symbols, and emoji in the generated report. Use `references/guidelines.md` for execution constraints, prioritization rules, and style. +After Phase 5 completes successfully, you may follow up with a short message pointing the user to the generated HTML report (one paragraph, no need to repeat the summary). ## Validation -- Verify that at least one `coverage.cobertura.xml` file was generated after `dotnet test` +- Verify that at least one `coverage.cobertura.xml` file was generated after `dotnet test` (or already exists when the user supplied one) +- Confirm the assistant response contained the CRAP/risk-hotspot table — saving the markdown file is secondary - Confirm `TestResults/coverage-analysis/coverage-analysis.md` was written and contains data - Spot-check one method's CRAP score: `comp² × (1 − cov)³ + comp` — a method with 100% coverage should have CRAP = complexity -- If ReportGenerator ran, verify `TestResults/coverage-analysis/reports/index.html` exists +- If Phase 5 ran, verify `TestResults/coverage-analysis/reports/index.html` exists; otherwise the report file should mark HTML/Text/CSV rows as `Not generated` ## Common Pitfalls - **No Cobertura XML generated** — the test project may lack a coverage provider. The skill auto-adds one, but if `dotnet add package` fails (offline/proxy), coverage collection silently produces nothing. Check for `.coverage` binary files as a fallback indicator. - **Test failures (exit code 1)** — coverage is still collected from passing tests. Do not abort; proceed with partial data and note the failures in the summary. -- **ReportGenerator install failure** — if `dotnet tool install` fails (no internet), skip HTML/CSV report generation and continue with raw Cobertura XML analysis + script-based CRAP scores. Note the skip in the report. +- **Premature end before user-facing summary** — never start Phase 5 (ReportGenerator install/run) before the Phase 4 assistant response is delivered. The heavy `dotnet tool install` can crash the session or exhaust budget, leaving the user with no analysis even though the CRAP scores were already computed. +- **ReportGenerator install failure** — if `dotnet tool install` fails (no internet) during Phase 5, leave the existing Phase 4 summary as the final output and note that HTML reports were skipped. Do not retry or block on the install. - **Method name mismatches in Cobertura** — async methods, lambdas, and local functions may have compiler-generated names. The scripts use the Cobertura method name/signature directly; verify against source if results look unexpected. - **Mixed coverage providers** — when a solution contains both Coverlet and Microsoft CodeCoverage projects, the skill runs per-project to avoid dual-provider conflicts. This is slower but correct. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/guidelines.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/guidelines.md index cb38224..344f69e 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/guidelines.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/guidelines.md @@ -2,7 +2,7 @@ **Don't modify source or production code.** The only permitted project file modifications are adding a coverage provider package to test projects that currently have no provider: `coverlet.collector` (coverlet/mixed modes) or `Microsoft.Testing.Extensions.CodeCoverage` (ms-codecoverage mode). Do not add a second provider to projects that already have one. Always log package additions and document revert commands in the report. Write all other output to `TestResults/coverage-analysis/` under the test project directory. -**Always show and open the generated markdown report.** After writing `TestResults/coverage-analysis/coverage-analysis.md`, print its contents to the console and open the file in the current host editor/session automatically (when an editor is available). +**Always show and open the generated markdown report — but only after the assistant response with the CRAP/risk-hotspot summary has been delivered.** Saving and opening `TestResults/coverage-analysis/coverage-analysis.md` is a follow-up action; it must never delay the user-facing summary. **Don't generate new tests during the initial analysis run.** This skill surfaces where tests are needed. Test generation is a separate follow-up step outside the scope of this skill. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/output-format.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/output-format.md index c768806..7e3c5b6 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/output-format.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/references/output-format.md @@ -24,7 +24,8 @@ Copy the template below **verbatim** for all fixed elements (headings, table hea | **Test Result** | | — | ✅ / ⚠️ | > Coverage collected from ** of test project(s)**. -> Reports saved to: `/reports/` +> Outputs saved to: `/` (markdown summary + raw Cobertura XML). +> *If Phase 5 ran:* HTML/CSV reports also at `/reports/`. If any coverage provider package was added to test projects, include this note after the summary: @@ -75,9 +76,12 @@ Files below the line or branch coverage threshold, ordered by uncovered lines de | Report | Path | |--------|------| -| HTML (browsable) | `/reports/index.html` | -| Text summary | `/reports/Summary.txt` | -| GitHub markdown | `/reports/SummaryGithub.md` | -| CSV data | `/reports/Summary.csv` | -| Raw data | `/raw/` | +| Markdown summary (this file) | `/coverage-analysis.md` | +| Raw Cobertura XML | `` | +| HTML (browsable) | `/reports/index.html` *or* `Not generated (optional — request HTML reports to enable)` | +| Text summary | `/reports/Summary.txt` *or* `Not generated` | +| GitHub markdown | `/reports/SummaryGithub.md` *or* `Not generated` | +| CSV data | `/reports/Summary.csv` *or* `Not generated` | ``` + +If ReportGenerator (Phase 5) has not run, mark the HTML/Text/GitHub-markdown/CSV rows as `Not generated (optional — request HTML reports to enable)`. Do not invent paths for files that have not been produced. For **Raw Cobertura XML**, list the actual XML file path(s) used in analysis (for from-scratch runs this is typically under `/raw/`; for existing-data runs this may be under `TestResults/` or another user-supplied location). diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 index a4b1799..b0c8d9f 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 @@ -8,8 +8,11 @@ # .\Compute-CrapScores.ps1 -CoberturaPath ,,... [-CrapThreshold ] [-TopN ] # # Outputs: -# - Hotspot rows (top N by CRAP score) as a JSON array to stdout (HOTSPOTS:) -# - Summary counts as TOTAL_METHODS: and FLAGGED_METHODS: +# - OVERALL_LINE_COVERAGE: (aggregate line coverage across input files, as percent) +# - OVERALL_BRANCH_COVERAGE: (aggregate branch coverage across input files, as percent) +# - TOTAL_METHODS: +# - FLAGGED_METHODS: +# - HOTSPOTS: (top N by CRAP score) param( [Parameter(Mandatory)][string[]]$CoberturaPath, @@ -18,8 +21,16 @@ param( ) # Merge methods across all Cobertura files using a stable key (Class|Method|Signature|File). -# Line hits are accumulated so a line is counted as covered if any test project covered it. +# Line hits are accumulated so a line is counted as covered if any input coverage file covered it. $methodMap = @{} +$overallLineRate = 0.0 +$overallBranchRate = 0.0 +$totalLinesCovered = 0 +$totalLinesValid = 0 +$totalBranchesCovered = 0 +$totalBranchesValid = 0 +$fallbackLineRates = [System.Collections.Generic.List[double]]::new() +$fallbackBranchRates = [System.Collections.Generic.List[double]]::new() foreach ($filePath in $CoberturaPath) { if (-not (Test-Path $filePath)) { @@ -34,6 +45,20 @@ foreach ($filePath in $CoberturaPath) { exit 2 } + # Prefer aggregate numerator/denominator attributes when present. + if ($null -ne $cobertura.coverage.'lines-covered' -and $null -ne $cobertura.coverage.'lines-valid') { + $totalLinesCovered += [double]$cobertura.coverage.'lines-covered' + $totalLinesValid += [double]$cobertura.coverage.'lines-valid' + } elseif ($cobertura.coverage.'line-rate') { + $fallbackLineRates.Add([double]$cobertura.coverage.'line-rate') + } + if ($null -ne $cobertura.coverage.'branches-covered' -and $null -ne $cobertura.coverage.'branches-valid') { + $totalBranchesCovered += [double]$cobertura.coverage.'branches-covered' + $totalBranchesValid += [double]$cobertura.coverage.'branches-valid' + } elseif ($cobertura.coverage.'branch-rate') { + $fallbackBranchRates.Add([double]$cobertura.coverage.'branch-rate') + } + foreach ($package in $cobertura.coverage.packages.package) { foreach ($class in $package.classes.class) { $className = $class.name @@ -104,6 +129,33 @@ foreach ($entry in $methodMap.Values) { $hotspots = $results | Sort-Object CrapScore -Descending | Select-Object -First $TopN $flagged = $results | Where-Object { $_.CrapScore -gt $CrapThreshold } +if ($totalLinesValid -gt 0) { + $overallLineRate = $totalLinesCovered / $totalLinesValid +} else { + # Fallback approximation when Cobertura aggregate counters and per-file rates are unavailable. + # This uses merged method line totals and may under/over-estimate if Cobertura + # includes executable lines outside method nodes. + $mergedTotalLines = ($results | Measure-Object -Property TotalLines -Sum).Sum + $mergedCoveredLines = ($results | Measure-Object -Property CoveredLines -Sum).Sum + if ($mergedTotalLines -gt 0) { + $overallLineRate = [double]$mergedCoveredLines / [double]$mergedTotalLines + } elseif ($fallbackLineRates.Count -gt 0) { + $overallLineRate = ($fallbackLineRates | Measure-Object -Average).Average + } else { + $overallLineRate = 0.0 + } +} + +if ($totalBranchesValid -gt 0) { + $overallBranchRate = $totalBranchesCovered / $totalBranchesValid +} elseif ($fallbackBranchRates.Count -gt 0) { + $overallBranchRate = ($fallbackBranchRates | Measure-Object -Average).Average +} else { + $overallBranchRate = 0.0 +} + +Write-Host "OVERALL_LINE_COVERAGE:$([Math]::Round($overallLineRate * 100, 1))" +Write-Host "OVERALL_BRANCH_COVERAGE:$([Math]::Round($overallBranchRate * 100, 1))" Write-Host "TOTAL_METHODS:$($results.Count)" Write-Host "FLAGGED_METHODS:$($flagged.Count)" if ($hotspots) { diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/crap-score/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/crap-score/SKILL.md index 3825905..352dba4 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/crap-score/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/crap-score/SKILL.md @@ -1,13 +1,14 @@ --- name: crap-score description: > - Calculates CRAP (Change Risk Anti-Patterns) score for .NET methods, classes, - or files. Use when the user asks to assess test quality, identify risky - untested code, compute CRAP scores, or evaluate whether complex methods have - sufficient test coverage. Requires code coverage data (Cobertura XML) and - cyclomatic complexity analysis. - DO NOT USE FOR: writing tests, general test execution unrelated to coverage/CRAP - analysis, or general code coverage reporting without CRAP context. + Calculates targeted CRAP (Change Risk Anti-Patterns) scores for a named .NET + method, class, or single source file. Use when the user explicitly asks to + compute CRAP scores or assess risky untested code for a specific target, + combining Cobertura coverage data with cyclomatic complexity analysis. + DO NOT USE FOR: project-wide coverage analysis, coverage plateau or "stuck + coverage" diagnosis, what's blocking coverage, or where to add tests across + a project (use coverage-analysis); writing tests; running tests without + CRAP context. license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/detect-static-dependencies/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/detect-static-dependencies/SKILL.md index 385b612..46bda03 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/detect-static-dependencies/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/detect-static-dependencies/SKILL.md @@ -24,6 +24,12 @@ Scan a C# codebase for calls to hard-to-test static APIs and produce a ranked re - Prioritizing which statics to wrap first (highest-frequency wins) - Creating a migration plan for incremental testability improvements +## Response Guidelines + +- Scale the response to the user's request. A question about a specific category (e.g., "find time statics") should focus on that category with file locations and counts, not produce a full report across all categories. +- When the user provides a specific file or directory path, scan only that scope — do not expand to the entire solution unless asked. +- The full structured report format in Step 4 is for comprehensive audit requests. For focused questions, return only the relevant subset (e.g., category summary + affected files for the requested category). + ## When Not to Use - The user wants wrappers generated (hand off to `generate-testability-wrappers`) diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-mstest-v1v2-to-v3/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-mstest-v1v2-to-v3/SKILL.md index 5667a0f..ca7fea4 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-mstest-v1v2-to-v3/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-mstest-v1v2-to-v3/SKILL.md @@ -34,7 +34,7 @@ Migrate a test project from MSTest v1 (assembly references) or MSTest v2 (NuGet ## When Not to Use -- Project already uses MSTest v3 (3.x packages) +- Project already on MSTest v3 with no migration-related build errors (fully migrated) - Upgrading v3 to v4 -- use `migrate-mstest-v3-to-v4` - Migrating between frameworks (MSTest to xUnit/NUnit) diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-vstest-to-mtp/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-vstest-to-mtp/SKILL.md index 1e002d3..d1d8928 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-vstest-to-mtp/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-vstest-to-mtp/SKILL.md @@ -4,17 +4,17 @@ description: > Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", or mentions EnableMSTestRunner, - EnableNUnitRunner, UseMicrosoftTestingPlatformRunner, dotnet test exit - code 8, zero tests discovered, or MTP behavioral differences - (--ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE). + EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. + USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests + discovered), --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE. Supports MSTest, NUnit, xUnit.net v2 (via YTest.MTP.XUnit2), and xUnit.net v3 (native MTP). Covers runner enablement, CLI argument - translation, xUnit.net v3 filter syntax, Directory.Build.props and - global.json configuration, CI/CD pipeline updates, and MTP extension - packages. DO NOT USE FOR: migrating between test frameworks - (MSTest/xUnit/NUnit), xUnit.net v2 to v3 API migration, MSTest version - upgrades (use migrate-mstest-* skills), TFM upgrades, or UWP/WinUI test - projects. + translation, xUnit.net v3 filter migration (--filter-class, + --filter-trait, --filter-query), Directory.Build.props and global.json + configuration, CI/CD pipeline updates, and MTP extension packages. + DO NOT USE FOR: migrating between test frameworks (MSTest/xUnit/NUnit), + xUnit.net v2 to v3 API migration, MSTest version upgrades (use + migrate-mstest-* skills), TFM upgrades, or UWP/WinUI test projects. license: MIT --- @@ -35,7 +35,7 @@ Migrate a .NET test solution from VSTest to Microsoft.Testing.Platform (MTP). Th ## When Not to Use -- The project already runs on Microsoft.Testing.Platform -- migration is done +- The project already runs on Microsoft.Testing.Platform and there is no remaining MTP behavioral difference to resolve (e.g., exit code 8 for zero tests discovered) - Migrating between test frameworks (e.g., MSTest to xUnit.net) -- different effort entirely - The project builds UWP or packaged WinUI test projects -- MTP does not support these yet - The solution mixes .NET and non-.NET test adapters (e.g., JavaScript or C++ adapters) -- VSTest is required @@ -208,7 +208,44 @@ VSTest-specific arguments must be translated to MTP equivalents. Build-related a **MSTest, NUnit, and xUnit.net v2 (with `YTest.MTP.XUnit2`)**: The VSTest `--filter` syntax is identical on both VSTest and MTP. No changes needed. -**xUnit.net v3 (native MTP)**: xUnit.net v3 does NOT support the VSTest `--filter` syntax on MTP. See the **VSTest → MTP filter translation** section in the `filter-syntax` skill for the complete translation table. Key translation example: +**xUnit.net v3 (native MTP)**: xUnit.net v3 does NOT support the VSTest `--filter` syntax on MTP. You must translate filters to xUnit.net v3's native filter options. + +#### xUnit.net v3 filter flags + +| Flag | Description | +|------|-------------| +| `--filter-class "name"` | Run all tests in a given class. Supports wildcards (`*`). | +| `--filter-not-class "name"` | Exclude all tests in a given class | +| `--filter-method "name"` | Run a specific test method | +| `--filter-not-method "name"` | Exclude a specific test method | +| `--filter-namespace "name"` | Run all tests in a namespace | +| `--filter-not-namespace "name"` | Exclude all tests in a namespace | +| `--filter-trait "name=value"` | Run tests with a matching trait | +| `--filter-not-trait "name=value"` | Exclude tests with a matching trait | + +Multiple values can be specified with a single flag: `--filter-class Foo Bar`. + +#### VSTest → xUnit.net v3 filter translation table + +| VSTest `--filter` syntax | xUnit.net v3 MTP equivalent | Notes | +|---|---|---| +| `FullyQualifiedName~ClassName` | `--filter-class *ClassName*` | Wildcards required for substring match | +| `FullyQualifiedName=Ns.Class.Method` | `--filter-method Ns.Class.Method` | Exact match on fully qualified method | +| `Name=MethodName` | `--filter-method *MethodName*` | Wildcards for substring match | +| `Category=Value` (trait) | `--filter-trait "Category=Value"` | Filter by trait name/value pair | +| Complex expressions | `--filter-query "expr"` | Uses xUnit.net query filter language (see below) | + +#### xUnit.net v3 query filter language + +For complex expressions, use `--filter-query` with a path-segment syntax: + +```text +////[traitName=traitValue] +``` + +Each segment matches against: assembly name, namespace, class name, method name. Use `*` for "match all" in any segment. Documentation: + +#### Translation example ```shell # VSTest diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-xunit-to-xunit-v3/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-xunit-to-xunit-v3/SKILL.md index 6d2beb6..759ce64 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-xunit-to-xunit-v3/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-xunit-to-xunit-v3/SKILL.md @@ -5,7 +5,9 @@ description: > USE FOR: upgrading xunit to xunit.v3. DO NOT USE FOR: migrating between test frameworks (MSTest/NUnit to xUnit.net), migrating from VSTest to Microsoft.Testing.Platform - (use migrate-vstest-to-mtp). + (use migrate-vstest-to-mtp). For xUnit v3 MTP filter syntax + (--filter-class, --filter-trait, --filter-query), also load + migrate-vstest-to-mtp. license: MIT --- @@ -34,7 +36,9 @@ Migrate .NET test projects from xUnit.net v2 to xUnit.net v3. The outcome is a s > **Commit strategy:** Commit after each major step so the migration is reviewable and bisectable. Separate project file changes from code changes. -### Step 1: Identify xUnit.net projects +> **Prioritization:** Steps 1-5 are required for every migration. Steps 6-12 are conditional — only apply the ones relevant to the project's code patterns. Skip steps that don't apply. + +### Step 1: Identify xUnit.net projects and verify compatibility Search for test projects referencing xUnit.net v2 packages: @@ -48,20 +52,9 @@ Search for test projects referencing xUnit.net v2 packages: Make sure to check the package references in project files, MSBuild props and targets files, like `Directory.Build.props`, `Directory.Build.targets`, and `Directory.Packages.props`. -### Step 2: Verify compatibility - -1. Verify target framework compatibility: xUnit.net v3 requires **.NET 8+** or **.NET Framework 4.7.2+**. For test library projects, .NET Standard 2.0 is also supported. -2. If any of the test projects have non-compatible target frameworks, STOP here and DON'T do anything. Only tell the user to upgrade the target framework first before migrating xUnit.net. -3. Verify project compatibility: xUnit.net v3 only supports SDK-style projects. If any test projects are non-SDK-style, STOP here and DON'T do anything. Only tell the user to migrate to SDK-style projects first before migrating xUnit.net. - -### Step 3: Establish a baseline - -Run `dotnet test` to establish a baseline of test pass/fail counts. When running `dotnet test`, ensure that: - -- You run `dotnet test` without any additional arguments (i.e., don't pass `--no-restore` or `--no-build`). -- Ensure you redirect the command output to a file and read the output from that file. +Verify target framework compatibility: xUnit.net v3 requires **.NET 8+** or **.NET Framework 4.7.2+**. For test library projects, .NET Standard 2.0 is also supported. If any test projects have non-compatible target frameworks, STOP here — tell the user to upgrade the target framework first. Also verify the project uses SDK-style format. -### Step 4: Update package references +### Step 2: Update package references 1. Update any `PackageReference` or `PackageVersion` items for the new package names, based on the following mapping: @@ -73,7 +66,7 @@ Run `dotnet test` to establish a baseline of test pass/fail counts. When running 2. Update all `xunit.v3.*` packages to the latest correct version available on NuGet. Also update `xunit.runner.visualstudio` to the latest version. -### Step 5: Set `OutputType` to `Exe` +### Step 3: Set `OutputType` to `Exe` In each test project (excluding test library projects), set `OutputType` to `Exe` in the project file: @@ -89,15 +82,25 @@ Depending on the solution in hand, there might be a centralized place where this - If all test projects share a name pattern (e.g., `*.Tests.csproj`), add a conditional property group in `Directory.Build.props` that applies only to those projects, like `Exe`. Adjust the condition as needed to target only test projects. - Otherwise, add the `Exe` property to each test project file individually. -### Step 6: Remove `Xunit.Abstractions` usings +### Step 4: Configure test platform + +Preserve the same test platform that was used with xUnit.net v2. xUnit.net v2 always uses VSTest except if the project used `YTest.MTP.XUnit2`. + +- If the project had a reference to `YTest.MTP.XUnit2`: + - Remove the reference to `YTest.MTP.XUnit2` completely. + - Add `true` to `Directory.Build.props` under an unconditional `PropertyGroup`. +- If the project did NOT reference `YTest.MTP.XUnit2` (the common case): + - Add `false` to `Directory.Build.props` under an unconditional `PropertyGroup`. If `Directory.Build.props` doesn't exist, create it. This keeps the project on VSTest. + +### Step 5: Remove `Xunit.Abstractions` usings Find any `using Xunit.Abstractions;` directives in C# files and remove them completely. -### Step 7: Address `async void` breaking change +### Step 6: Address `async void` breaking change (if applicable) In xUnit.net v3, `async void` test methods are no longer supported and will fail to compile. Search for any test methods declared with `async void` and change them to `async Task`. Test methods can be identified via the `[Fact]` or `[Theory]` attributes or other test attributes. -### Step 8: Address breaking change of attributes +### Step 7: Address breaking change of attributes (if applicable) In xUnit.net v3, some attributes were updated so that they accept a `System.Type` instead of two strings (fully qualified type name and assembly name). These attributes are: @@ -108,7 +111,7 @@ In xUnit.net v3, some attributes were updated so that they accept a `System.Type For example, `[assembly: CollectionBehavior("MyNamespace.MyCollectionFactory", "MyAssembly")]` must be converted to `[assembly: CollectionBehavior(typeof(MyNamespace.MyCollectionFactory))]`. -### Step 9: Inheriting from FactAttribute or TheoryAttribute +### Step 8: Inheriting from FactAttribute or TheoryAttribute (if applicable) Identify if there are any custom attributes that inherit from `FactAttribute` or `TheoryAttribute`. These custom user-defined attributes must now provide source information. For example, if the attribute looked like this: @@ -135,7 +138,7 @@ internal sealed class MyFactAttribute : FactAttribute } ``` -### Step 10: Inheriting from BeforeAfterTestAttribute +### Step 9: Inheriting from BeforeAfterTestAttribute (if applicable) Identify if there are any custom attributes that inherit from `BeforeAfterTestAttribute`. These custom user-defined attributes must update their method signatures. Previously, they would have `Before`/`After` overrides that look like this: @@ -173,25 +176,11 @@ it must be changed to this: } ``` -### Step 11: Address new xUnit analyzer warnings - -xunit.v3 introduced new analyzer warnings. You should attempt to address them. +### Step 10: Address new xUnit analyzer warnings (if applicable) -One of the most notable warnings is [xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken](https://xunit.net/xunit.analyzers/rules/xUnit1051). Identify the calls to such methods, if any, and pass the cancellation token. +xunit.v3 introduced new analyzer warnings. The most notable is xUnit1051 (use `TestContext.Current.CancellationToken` for methods accepting `CancellationToken`). Address these if present. -### Step 12: Test platform selection - -You should keep the same test platform that was used with xunit 2. - -Note that xunit 2 is always VSTest except if the user used YTest.MTP.XUnit2. - -- If user had a reference to YTest.MTP.XUnit2: - - Remove the reference to YTest.MTP.XUnit2 completely. - - Add `true` to Directory.Build.props under an unconditional PropertyGroup. -- If user didn't have a reference to YTest.MTP.XUnit2: - - Add `false` to Directory.Build.props under an unconditional PropertyGroup. - -### Step 13: Migrate `Xunit.SkippableFact` +### Step 11: Migrate `Xunit.SkippableFact` (if applicable) If there are any package references to `Xunit.SkippableFact`, remove all these package references entirely. @@ -202,19 +191,11 @@ Then, follow these steps to eliminate usages of APIs coming from the removed pac - Change `Skip.If` method calls to `Assert.SkipWhen`. - Change `Skip.IfNot` method calls to `Assert.SkipUnless`. -### Step 14: Update `Xunit.Combinatorial` NuGet package - -Find package references of `Xunit.Combinatorial` and update them from 1.x to the latest 2.x version available. - -### Step 15: Update `Xunit.StaFact` NuGet package - -Find package references of `Xunit.StaFact` and update them from 1.x to the latest 3.x version available. - -### Step 16: Build the solution +### Step 12: Update companion packages (if applicable) -Now, build the solution to identify any remaining compilation errors that might not have been addressed by previous instructions. -Fix any straightforward errors that show up, and keep iterating and fixing more. +- `Xunit.Combinatorial` 1.x → latest 2.x +- `Xunit.StaFact` 1.x → latest 3.x -You can also look into and to help with the remaining compilation errors. +### Step 13: Build and verify -You can fix as much as you can, and it's okay if not everything is fixed. Just tell the user that there are remaining errors that need to be manually addressed. +Build the solution and fix any remaining compilation errors. Run `dotnet test` to verify all tests pass with the same results as before migration. diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-anti-patterns/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-anti-patterns/SKILL.md index 6cbbbfa..762da91 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-anti-patterns/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-anti-patterns/SKILL.md @@ -1,6 +1,18 @@ --- name: test-anti-patterns -description: "Quick pragmatic detection-focused review of .NET test code for anti-patterns that undermine reliability and diagnostic value. Use when asked to audit test quality, investigate flaky or coupled tests, find duplication or magic values, or when tests pass but don't actually verify anything. Best for identifying and prioritizing issues in existing tests with severity-ranked findings and targeted remediation guidance. Catches assertion gaps, swallowed exceptions, always-true assertions, flakiness indicators, test coupling, over-mocking, naming issues, magic values, duplicate tests, and structural problems. Do NOT use for direct MSTest API rewrites or implementation-only fixes (for example swapped Assert.AreEqual argument order or converting `DynamicData` from `IEnumerable` to `ValueTuple`) — use writing-mstest-tests instead. For a deep formal audit based on academic test smell taxonomy, use exp-test-smell-detection instead. Works with MSTest, xUnit, NUnit, and TUnit." +description: > + Detection-focused review of .NET test code for anti-patterns that + undermine reliability and diagnostic value. + USE FOR: audit test quality, review test code, find test anti-patterns, + tests pass but don't verify anything, flaky tests, ordering dependency, + duplicate tests, magic values, missing/no assertions, swallowed + exceptions, always-true assertions, over-mocking, test coupling, coverage + touching, coverage inflation. + DO NOT USE FOR: writing new tests (use writing-mstest-tests), direct + MSTest API rewrites or implementation-only fixes such as swapped + Assert.AreEqual argument order, running tests (use run-tests), migrating + between frameworks (use migration skills), deep formal audit based on + academic test smell taxonomy (use test-smell-detection). license: MIT --- @@ -25,7 +37,7 @@ Quick, pragmatic analysis of .NET test code for anti-patterns and quality issues - User wants to run or execute tests (use `run-tests`) - User wants to migrate between test frameworks or versions (use migration skills) - User wants to measure code coverage (out of scope) -- User wants a deep formal test smell audit with academic taxonomy and extended catalog (use `exp-test-smell-detection`) +- User wants a deep formal test smell audit with academic taxonomy and extended catalog (use `test-smell-detection`) ## Inputs @@ -52,6 +64,8 @@ Check each test file against the anti-pattern catalog below. Report findings gro | Anti-Pattern | What to Look For | |---|---| | **No assertions** | Test methods that execute code but never assert anything. A passing test without assertions proves nothing. | +| **Coverage touching** | Test class that methodically calls every public method on a type — often in alphabetical or declaration order — without asserting meaningful outcomes. Each test typically does `var result = sut.MethodName(...)` with no assertion, or only a trivial `Assert.IsNotNull(result)`. The intent is to inflate code-coverage metrics rather than verify behavior. Distinct from a single assertion-free test: the pattern is *systematic* coverage of the surface area with no real verification. | +| **Self-referential assertion** | Asserts that the output of an operation equals its input when the operation is expected to be an identity or no-op, e.g. `Assert.AreEqual(input, Parse(input.ToString()))` or `Assert.AreEqual(x, Identity(x))`. The test is tautological — it can only fail if the round-trip is broken, but it never verifies that a *transformation* actually happened. Also catches `Assert.AreEqual(dto.Name, dto.Name)` (asserting a field against itself). | | **Swallowed exceptions** | `try { ... } catch { }` or `catch (Exception)` without rethrowing or asserting. Failures are silently hidden. | | **Assert in catch block only** | `try { Act(); } catch (Exception ex) { Assert.Fail(ex.Message); }` -- use `Assert.ThrowsException` or equivalent instead. The test passes when no exception is thrown even if the result is wrong. | | **Always-true assertions** | `Assert.IsTrue(true)`, `Assert.AreEqual(x, x)`, or conditions that can never fail. | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-gap-analysis/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-gap-analysis/SKILL.md similarity index 98% rename from external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-gap-analysis/SKILL.md rename to external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-gap-analysis/SKILL.md index b5580be..734b1e3 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-gap-analysis/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-gap-analysis/SKILL.md @@ -1,6 +1,6 @@ --- -name: exp-test-gap-analysis -description: "Performs pseudo-mutation analysis on .NET production code to find gaps in existing test suites. Use when the user asks to find weak tests, discover untested edge cases, check if tests would catch a bug, or evaluate test effectiveness through mutation-style reasoning. Analyzes production code for mutation points (boundary conditions, boolean flips, null returns, exception removal, arithmetic changes) and checks whether existing tests would detect each mutation. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), detecting test anti-patterns (use test-anti-patterns), measuring assertion diversity (use exp-assertion-quality), or running actual mutation testing tools." +name: test-gap-analysis +description: "Performs pseudo-mutation analysis on .NET production code to find gaps in existing test suites. Use when the user asks to find weak tests, discover untested edge cases, check if tests would catch a bug, or evaluate test effectiveness through mutation-style reasoning. Analyzes production code for mutation points (boundary conditions, boolean flips, null returns, exception removal, arithmetic changes) and checks whether existing tests would detect each mutation. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), detecting test anti-patterns (use test-anti-patterns), measuring assertion diversity (use assertion-quality), or running actual mutation testing tools." license: MIT --- @@ -35,7 +35,7 @@ This skill performs **static pseudo-mutation** — reasoning about mutations wit - User wants to write new tests from scratch (use `writing-mstest-tests`) - User wants to detect test anti-patterns like flakiness or poor naming (use `test-anti-patterns`) -- User wants to measure assertion variety (use `exp-assertion-quality`) +- User wants to measure assertion variety (use `assertion-quality`) - User wants to run an actual mutation testing framework like Stryker (help them directly) - User only wants code coverage numbers (out of scope) diff --git a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-smell-detection/SKILL.md similarity index 94% rename from catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/SKILL.md rename to external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-smell-detection/SKILL.md index 546cd98..148f85f 100644 --- a/catalog/Platform/Official-DotNet-Experimental/skills/exp-test-smell-detection/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-smell-detection/SKILL.md @@ -1,6 +1,6 @@ --- -name: exp-test-smell-detection -description: "Deep formal test smell audit based on academic research taxonomy (testsmells.org). Detects 19 categorized smell types — conditional logic, mystery guests, sensitive equality, eager tests, and more — with calibrated severity and research-backed remediation. Use for comprehensive test suite health assessments. For a quick pragmatic review, use test-anti-patterns instead. DO NOT USE FOR: writing new tests (use writing-mstest-tests), evaluating assertion quality specifically (use exp-assertion-quality), or finding test duplication and boilerplate (use exp-test-maintainability)." +name: test-smell-detection +description: "Deep formal test smell audit based on academic research taxonomy (testsmells.org). Detects 19 categorized smell types — conditional logic, mystery guests, sensitive equality, eager tests, and more — with calibrated severity and research-backed remediation. Use for comprehensive test suite health assessments. For a quick pragmatic review, use test-anti-patterns instead. DO NOT USE FOR: writing new tests (use writing-mstest-tests), evaluating assertion quality specifically (use assertion-quality), or finding test duplication and boilerplate (use exp-test-maintainability)." license: MIT --- @@ -34,7 +34,7 @@ Test smells erode confidence in a test suite and inflate maintenance costs: ## When Not to Use - User wants a quick pragmatic test review (use `test-anti-patterns` — faster, covers the most common issues) -- User wants to evaluate assertion diversity specifically (use `exp-assertion-quality`) +- User wants to evaluate assertion diversity specifically (use `assertion-quality`) - User wants to find duplicated boilerplate across tests (use `exp-test-maintainability`) - User wants to write new tests from scratch (help them directly) - User wants to fix a specific failing test (diagnose and fix directly) @@ -50,7 +50,7 @@ Test smells erode confidence in a test suite and inflate maintenance costs: ### Step 1: Gather the test code -Read all test files the user provides. If the user points to a directory or project, scan for all test files by looking for test framework markers — see the `exp-dotnet-test-frameworks` skill for .NET-specific markers. +Read all test files the user provides. If the user points to a directory or project, scan for all test files by looking for test framework markers — see the `dotnet-test-frameworks` skill for .NET-specific markers. For a thorough audit, also consult the [extended smell catalog](references/test-smell-catalog.md) which covers 9 additional smell types beyond the core 10 below. @@ -79,7 +79,7 @@ Tests that depend on external resources — files on disk, databases, network en Tests that call sleep or delay functions to wait for a condition. These introduce non-deterministic timing and slow down the suite. **Severity:** High -**Detection:** Calls to sleep/delay functions inside test methods. See the `exp-dotnet-test-frameworks` skill for .NET-specific patterns. +**Detection:** Calls to sleep/delay functions inside test methods. See the `dotnet-test-frameworks` skill for .NET-specific patterns. #### Smell 4: Assertion-Free Test (Unknown Test) @@ -132,7 +132,7 @@ The test setup method or constructor initializes fields that are not used by eve Tests marked as skipped or disabled. These add overhead and clutter, and the underlying issue they were disabled for may never be addressed. **Severity:** Low -**Detection:** Skip/ignore annotations or conditional compilation that disables a test. See the `exp-dotnet-test-frameworks` skill for framework-specific skip attributes. +**Detection:** Skip/ignore annotations or conditional compilation that disables a test. See the `dotnet-test-frameworks` skill for framework-specific skip attributes. ### Step 3: Apply calibration rules @@ -192,7 +192,7 @@ Present the analysis in this structure: | Flagging integration tests for using real resources | Check for integration test markers and adjust severity accordingly | | Flagging loop-over-collection-assert as conditional logic | Only flag loops with branching or complex logic, not assertion iterations | | Flagging obvious count assertions after adding N items | Consider the immediate context — self-documenting numbers are fine | -| Missing framework-specific assertion syntax | Consult the `exp-dotnet-test-frameworks` skill for .NET framework assertion and skip APIs | +| Missing framework-specific assertion syntax | Consult the `dotnet-test-frameworks` skill for .NET framework assertion and skip APIs | | Over-flagging try/catch that captures for assertion | Distinguish swallowed exceptions from capture-and-assert patterns | | Treating skip annotations with reasons same as bare skips | Note that reasoned skips are less concerning than unexplained ones | | Flagging `DoesNotThrow`-style tests as assertion-free | These implicitly assert no exception — note but acknowledge the intent | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-smell-detection/references/test-smell-catalog.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-smell-detection/references/test-smell-catalog.md similarity index 100% rename from external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-smell-detection/references/test-smell-catalog.md rename to external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-smell-detection/references/test-smell-catalog.md diff --git a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-tagging/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-tagging/SKILL.md similarity index 98% rename from external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-tagging/SKILL.md rename to external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-tagging/SKILL.md index 0419a08..b423463 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-experimental/skills/exp-test-tagging/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/test-tagging/SKILL.md @@ -1,5 +1,5 @@ --- -name: exp-test-tagging +name: test-tagging description: "Analyzes test suites and tags each test with a standardized set of traits (e.g., positive, negative, critical-path, boundary, smoke, regression). Use when the user wants to categorize, audit, or label tests with traits. Do not use for writing new tests, running tests, or migrating test frameworks." license: MIT --- @@ -57,7 +57,7 @@ A single test may have **multiple traits** (e.g., both `negative` and `boundary` ### Step 1: Detect the test framework -Examine project files and source code to determine the framework — see the `exp-dotnet-test-frameworks` skill for the complete detection table (package references, test markers, assertion APIs, and skip annotations). +Examine project files and source code to determine the framework — see the `dotnet-test-frameworks` skill for the complete detection table (package references, test markers, assertion APIs, and skip annotations). ### Step 2: Scan existing traits diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md index b4b32a4..31cc944 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md @@ -1,6 +1,19 @@ --- name: writing-mstest-tests -description: "Best practices for writing new MSTest 3.x/4.x unit tests and implementing concrete fixes in existing MSTest code. Use when the user asks to write, create, implement, repair, or modernize tests (including fix-it prompts such as 'something seems off, fix issues'). Primary fit for direct code changes like correcting swapped Assert.AreEqual argument order, replacing outdated assertion patterns, and converting DynamicData from IEnumerable to ValueTuple-based data sets. Covers modern assertions, data-driven tests, test lifecycle, MSTest.Sdk, sealed classes, Assert.Throws, DynamicData with ValueTuples, TestContext, and conditional execution. Do NOT use for broad test quality audits, flaky-test investigations, or test smell detection reports — use test-anti-patterns instead." +description: > + Write new MSTest unit tests and implement concrete fixes in existing MSTest code using + MSTest 3.x/4.x modern APIs and best practices. + USE FOR: write unit tests for a class, write MSTest tests, create test class, + fix test assertions, MSTest assertion APIs (StartsWith, EndsWith, MatchesRegex, + IsGreaterThan, IsInRange, HasCount, IsNull), something seems off with my tests, + review tests and fix issues, + fix swapped Assert.AreEqual arguments, replace ExpectedException with Assert.Throws, modernize + test patterns, convert DynamicData to ValueTuples, data-driven tests, test lifecycle setup, + sealed test classes, async test patterns, cancellation token testing, + test parallelization, Parallelize, DoNotParallelize, MSTest.Sdk project setup. + DO NOT USE FOR: broad test quality audits or test smell detection (use test-anti-patterns), + running tests (use run-tests), MSTest version migration (use migrate-mstest-v1v2-to-v3 or + migrate-mstest-v3-to-v4). license: MIT --- @@ -13,6 +26,8 @@ Help users write effective, modern unit tests with MSTest 3.x/4.x using current - User wants to write new MSTest unit tests - User wants to improve or modernize existing MSTest tests by implementing concrete fixes - User asks about MSTest assertion APIs, data-driven patterns, or test lifecycle +- User asks to replace `Assert.IsTrue` with more specific assertions (collections, nulls, types, comparisons) +- User asks to replace hard casts with type-checking assertions in tests - User needs help fixing a specific MSTest test bug or failing assertion - User asks to fix swapped `Assert.AreEqual` argument order (expected first, actual second) - User asks to convert `DynamicData` from `IEnumerable` to ValueTuple-based data @@ -34,6 +49,12 @@ Help users write effective, modern unit tests with MSTest 3.x/4.x using current | Existing test code | No | Current tests to fix, update, or modernize | | Test scenario description | No | What behavior the user wants to test | +## Response Guidelines + +- **Specific API or pattern questions** (assertions, data-driven, lifecycle): Jump directly to the relevant workflow step. Do not follow the full workflow. +- **Write new tests from scratch**: Follow the full workflow. +- **Review and fix existing tests**: Fix only the issues present. Do not add unrelated improvements. + ## Workflow ### Step 1: Determine project setup @@ -109,13 +130,29 @@ public sealed class OrderServiceTests ### Step 3: Use modern assertion APIs -Use the correct assertion for each scenario. Prefer `Assert` class methods over `StringAssert` or `CollectionAssert` where both exist. +Pick the most specific assertion for each test scenario. More specific assertions produce better failure messages and make the test's intent clear: -#### Equality and null checks +| What you are testing | Assertion | +|---|---| +| Two values are equal | `Assert.AreEqual(expected, actual)` | +| Same object instance (reference identity) | `Assert.AreSame(expected, actual)` | +| Value is null | `Assert.IsNull(value)` | +| Value is not null | `Assert.IsNotNull(value)` | +| Collection is empty | `Assert.IsEmpty(collection)` | +| Collection is not empty | `Assert.IsNotEmpty(collection)` | +| Collection has exactly N items | `Assert.HasCount(N, collection)` | +| Collection contains an item | `Assert.Contains(item, collection)` | +| Collection does not contain an item | `Assert.DoesNotContain(item, collection)` | +| Object is a specific type | `Assert.IsInstanceOfType(value)` | +| Code throws an exception | `Assert.ThrowsExactly(() => ...)` | + +Prefer `Assert` class methods over `StringAssert` or `CollectionAssert` where both exist. + +#### Equality, null, and reference checks ```csharp Assert.AreEqual(expected, actual); // Value equality -Assert.AreSame(expected, actual); // Reference equality +Assert.AreSame(expected, actual); // Reference equality -- same object instance Assert.IsNull(value); Assert.IsNotNull(value); ``` @@ -151,8 +188,12 @@ Replace generic `Assert.IsTrue` with specialized assertions -- they give better | Instead of | Use | |---|---| | `Assert.IsTrue(list.Count > 0)` | `Assert.IsNotEmpty(list)` | +| `Assert.IsTrue(list.Count == 0)` | `Assert.IsEmpty(list)` | | `Assert.IsTrue(list.Count() == 3)` | `Assert.HasCount(3, list)` | | `Assert.IsTrue(x != null)` | `Assert.IsNotNull(x)` | +| `Assert.IsTrue(x == null)` | `Assert.IsNull(x)` | +| `Assert.AreEqual(a, b)` for same instance | `Assert.AreSame(a, b)` -- reference identity | +| `Assert.IsTrue(!list.Contains(item))` | `Assert.DoesNotContain(item, list)` | | `list.Single(predicate)` + `Assert.IsNotNull` | `Assert.ContainsSingle(list)` | | `Assert.IsTrue(list.Contains(item))` | `Assert.Contains(item, list)` | @@ -323,29 +364,3 @@ public void LocalOnly_InteractiveTest() { } [DoNotParallelize] // Opt out specific classes public sealed class DatabaseIntegrationTests { } ``` - -## Validation - -- [ ] Test classes are `sealed` -- [ ] Test methods follow `MethodName_Scenario_ExpectedBehavior` naming -- [ ] `Assert.ThrowsExactly` used instead of `[ExpectedException]` -- [ ] Specialized assertions used instead of `Assert.IsTrue` (e.g., `Assert.IsNotNull`, `Assert.AreEqual`) -- [ ] DynamicData uses ValueTuple return types instead of `IEnumerable` -- [ ] Sync initialization done in the constructor, not `[TestInitialize]` -- [ ] `TestContext.CancellationToken` passed to async calls in tests with `[Timeout]` -- [ ] Project builds with zero errors and all tests pass - -## Common Pitfalls - -| Pitfall | Solution | -|---------|----------| -| `Assert.AreEqual(actual, expected)` -- swapped arguments | Always put expected first: `Assert.AreEqual(expected, actual)`. Failure messages show "Expected: X, Actual: Y" so wrong order makes messages confusing | -| `[ExpectedException]` -- obsolete, cannot assert message | Use `Assert.Throws` or `Assert.ThrowsExactly` | -| `items.Single()` -- unclear exception on failure | Use `Assert.ContainsSingle(items)` for better failure messages | -| Hard cast `(MyType)result` -- unclear exception | Use `Assert.IsInstanceOfType(result)` | -| `IEnumerable` for DynamicData | Use `IEnumerable<(T1, T2, ...)>` ValueTuples for type safety | -| Sync setup in `[TestInitialize]` | Initialize in the constructor instead -- enables `readonly` fields and satisfies nullability analyzers | -| `CancellationToken.None` in async tests | Use `TestContext.CancellationToken` for cooperative timeout | -| `public TestContext? TestContext { get; set; }` | Drop the `?` -- MSTest suppresses CS8618 for this property | -| `TestContext TestContext { get; set; } = null!` | Remove `= null!` -- unnecessary, MSTest handles assignment | -| Non-sealed test classes | Seal test classes by default for performance | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md index 2844e88..5660c48 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/SKILL.md @@ -11,7 +11,7 @@ description: > Dockerfiles for .NET 11. DO NOT USE FOR: .NET Framework migrations, upgrading from .NET 9 or earlier, greenfield .NET 11 projects, or cosmetic modernization unrelated to the upgrade. - NOTE: .NET 11 is in preview. Covers breaking changes through Preview 1. + NOTE: .NET 11 is in preview. Covers breaking changes through Preview 3. license: MIT --- @@ -19,7 +19,7 @@ license: MIT Migrate a .NET 10 project or solution to .NET 11, systematically resolving all breaking changes. The outcome is a project targeting `net11.0` that builds cleanly, passes tests, and accounts for every behavioral, source-incompatible, and binary-incompatible change introduced in .NET 11. -> **Note:** .NET 11 is currently in preview. This skill covers breaking changes documented through Preview 1. It will be updated as additional previews ship. +> **Note:** .NET 11 is currently in preview. This skill covers breaking changes documented through Preview 3. ## When to Use @@ -59,10 +59,14 @@ Migrate a .NET 10 project or solution to .NET 11, systematically resolving all b - **SDK attribute**: `Microsoft.NET.Sdk.Web` → ASP.NET Core; `Microsoft.NET.Sdk.WindowsDesktop` with `` or `` → WPF/WinForms - **PackageReferences**: `Microsoft.EntityFrameworkCore.*` → EF Core; `Microsoft.EntityFrameworkCore.Cosmos` → Cosmos DB provider - **Dockerfile presence** → Container changes relevant - - **Cryptography API usage** → DSA on macOS affected + - **Cryptography API usage** → DSA on macOS affected; AIA cert download changes relevant - **Compression API usage** → DeflateStream/GZipStream/ZipArchive changes relevant - - **TAR API usage** → Header checksum validation change relevant + - **TAR API usage** → Header checksum validation and HardLink entry changes relevant - **`NamedPipeClientStream` usage with `SafePipeHandle`** → SYSLIB0063 constructor obsoletion relevant + - **`BackgroundService` usage** → Unhandled exceptions now stop the host + - **`Microsoft.OpenApi` direct usage** → v3 API breaking changes in ASP.NET Core OpenAPI + - **EF Core SQL Server with Entra ID auth** → SqlClient 7.0 auth dependency changes + - **NativeAOT native libraries on Unix** → Output filename prefix changed 4. Record which reference documents are relevant (see the reference loading table in Step 3). 5. Do a **clean build** (`dotnet build --no-incremental` or delete `bin`/`obj`) on the current `net10.0` target to establish a clean baseline. Record any pre-existing warnings. @@ -93,9 +97,10 @@ Load reference documents based on the project's technology areas: | `references/csharp-compiler-dotnet10to11.md` | Always (C# 15 compiler breaking changes) | | `references/core-libraries-dotnet10to11.md` | Always (applies to all .NET 11 projects) | | `references/sdk-msbuild-dotnet10to11.md` | Always (SDK and build tooling changes) | -| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core (especially Cosmos DB provider) | -| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs or targets macOS | -| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware or embedded devices | +| `references/aspnetcore-dotnet10to11.md` | Project uses ASP.NET Core (OpenAPI, Blazor) | +| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core | +| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs, mTLS, or targets macOS | +| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware, embedded devices, or using NativeAOT | Work through each build error systematically. Common patterns: @@ -115,6 +120,12 @@ Work through each build error systematically. Common patterns: 8. **`when` switch-expression-arm parsing** — `(X.Y) when` is now parsed as a constant pattern with a `when` clause instead of a cast expression, which can cause existing code to fail to compile or change meaning. Review switch expressions using `when` and adjust syntax as needed. +9. **Microsoft.OpenApi v3 breaking changes** — `Microsoft.AspNetCore.OpenApi` now depends on `Microsoft.OpenApi` 3.x. Code using `Microsoft.OpenApi` types directly (`OpenApiDocument`, `OpenApiSchema`, etc.) will have compile errors. Follow the v3 upgrade guide. + +10. **EF Core Design package no longer transitive** — `Microsoft.EntityFrameworkCore.Tools` and `.Tasks` no longer depend on `.Design`. Add an explicit `PackageReference` if needed. + +11. **EFOptimizeContext MSBuild property removed** — Replace with `` and ``. + ### Step 4: Address behavioral changes These changes compile successfully but alter runtime behavior. Review each one and determine impact: @@ -137,6 +148,24 @@ These changes compile successfully but alter runtime behavior. Review each one a 9. **Mono launch target for .NET Framework** — No longer set automatically. If using Mono for .NET Framework apps on Linux, specify explicitly. +10. **Unhandled BackgroundService exceptions stop the host** — Exceptions from `ExecuteAsync()` now propagate and crash the host. Add try/catch in background services that should not bring down the application. + +11. **ZipArchive CRC32 validation** — ZIP reads now validate CRC32 checksums. Corrupt or truncated archives that previously succeeded will now throw `InvalidDataException`. + +12. **TarWriter emits HardLink entries** — Hard-linked files are now written as `HardLink` entries instead of duplicated data. Consumers of .NET-produced tar archives must handle `HardLink` entries. + +13. **AIA certificate downloads disabled** — Server-side client-certificate validation no longer downloads intermediate CAs via AIA by default. Pre-install the full chain or have clients send intermediates. + +14. **Blazor Virtualize OverscanCount default changed** — Default `OverscanCount` changed from 3 to 15. Set explicitly if performance-sensitive. + +15. **Microsoft.Data.SqlClient 7.0 — Entra ID auth separated** — Azure/Entra ID authentication dependencies removed from the core SqlClient package. Add `Microsoft.Data.SqlClient.Extensions.Azure` if using Entra ID auth. + +16. **SqlVector<T> excluded from SELECT** — Vector properties are no longer auto-loaded. Use explicit projections to include vector values. + +17. **SQLitePCLRaw encryption bundles removed** — `bundle_e_sqlcipher` and other encryption bundle packages removed in SQLitePCLRaw 3.0. + +18. **NativeAOT Unix native library `lib` prefix** — Output filenames now include `lib` prefix on Linux/macOS (e.g., `libMyLib.so`). + ### Step 5: Update infrastructure 1. **Dockerfiles**: Update base images from 10.0 to 11.0: @@ -155,7 +184,7 @@ These changes compile successfully but alter runtime behavior. Review each one a "sdk": { - "version": "10.0.100", - "rollForward": "latestFeature" - + "version": "11.0.100-preview.1", + + "version": "11.0.100-preview.3", + "rollForward": "latestFeature" }, "otherSettings": { @@ -173,11 +202,15 @@ These changes compile successfully but alter runtime behavior. Review each one a 3. If the application is containerized, build and test the container image 4. Smoke-test the application, paying special attention to: - Compression behavior with empty streams - - TAR file reading + - TAR file reading (checksum validation and HardLink entries) - EF Core Cosmos DB operations (must be async) - DSA usage on macOS - Memory-intensive MemoryStream usage - Span collection expression assignments + - BackgroundService exception handling + - mTLS / client certificate chain validation + - EF Core SQL Server with Entra ID authentication + - NativeAOT output filenames on Unix 5. Review the diff and ensure no unintended behavioral changes were introduced ## Reference Documents @@ -189,6 +222,7 @@ The `references/` folder contains detailed breaking change information organized | `references/csharp-compiler-dotnet10to11.md` | Always (C# 15 compiler breaking changes) | | `references/core-libraries-dotnet10to11.md` | Always (applies to all .NET 11 projects) | | `references/sdk-msbuild-dotnet10to11.md` | Always (SDK and build tooling changes) | -| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core (especially Cosmos DB provider) | -| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs or targets macOS | -| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware or embedded devices | +| `references/aspnetcore-dotnet10to11.md` | Project uses ASP.NET Core (OpenAPI, Blazor) | +| `references/efcore-dotnet10to11.md` | Project uses Entity Framework Core | +| `references/cryptography-dotnet10to11.md` | Project uses cryptography APIs, mTLS, or targets macOS | +| `references/runtime-jit-dotnet10to11.md` | Deploying to older hardware, embedded devices, or using NativeAOT | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/aspnetcore-dotnet10to11.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/aspnetcore-dotnet10to11.md new file mode 100644 index 0000000..bc97680 --- /dev/null +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/aspnetcore-dotnet10to11.md @@ -0,0 +1,27 @@ +# ASP.NET Core Breaking Changes (.NET 11) + +These breaking changes affect ASP.NET Core projects. Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/11 + +> **Note:** .NET 11 is in preview. Additional ASP.NET Core breaking changes are expected in later previews. + +## Source-Incompatible Changes + +### Microsoft.OpenApi updated to v3 with OpenAPI 3.2.0 support (Preview 2) + +**Impact: Medium.** `Microsoft.AspNetCore.OpenApi` updated its dependency from `Microsoft.OpenApi` 2.x to 3.x, adding OpenAPI 3.2.0 document generation. The underlying `Microsoft.OpenApi` library has breaking API changes in the v2→v3 transition. + +Code that directly uses `Microsoft.OpenApi` types (`OpenApiDocument`, `OpenApiSchema`, `OpenApiOperation`, etc.) will have compile errors. + +**Fix:** Follow the [Microsoft.OpenApi v3 upgrade guide](https://github.com/microsoft/OpenAPI.NET/blob/main/docs/upgrade-guide-3.md). If you only use the ASP.NET Core OpenAPI integration (`.WithOpenApi()`, `MapOpenApi()`) without touching the object model directly, no changes are needed. + +Source: https://github.com/dotnet/aspnetcore/pull/65415 + +## Behavioral Changes + +### Blazor Virtualize<T> default OverscanCount changed from 3 to 15 (Preview 3) + +**Impact: Low.** The default `OverscanCount` on the `Virtualize` component changed from `3` to `15` to support variable-height item measurement. `QuickGrid` retains its own default of `3`. + +**Fix:** If performance-sensitive, set `OverscanCount` explicitly: ``. + +Source: https://github.com/dotnet/aspnetcore/pull/64964 diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md index d5a7122..ad45fbb 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/core-libraries-dotnet10to11.md @@ -85,3 +85,57 @@ Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-librari **Impact: Low.** The minimum supported date for the Japanese Calendar has been corrected. Code using very early dates in the Japanese Calendar may be affected. Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/globalization/11/japanese-calendar-min-date + +### ZipArchive now validates CRC32 when reading entries (Preview 3) + +**Impact: Low–Medium.** ZIP archive reads now validate the CRC32 checksum of each entry. Previously, corrupt or truncated archives were silently accepted; they now throw `InvalidDataException`. + +**Fix:** Ensure ZIP files are not corrupted. If processing partially-written or legacy archives, add error handling for `InvalidDataException`. + +Source: https://github.com/dotnet/runtime/pull/124766 + +### Unhandled BackgroundService exceptions now stop the host (Preview 3) + +**Impact: Medium.** Unhandled exceptions thrown from `BackgroundService.ExecuteAsync()` now propagate and stop the host application. Previously they were silently swallowed. + +```csharp +// .NET 10: exception silently swallowed, host continues +// .NET 11: exception propagates, host stops +protected override async Task ExecuteAsync(CancellationToken stoppingToken) +{ + throw new InvalidOperationException("oops"); // now kills the host +} + +// FIX: Add proper exception handling +protected override async Task ExecuteAsync(CancellationToken stoppingToken) +{ + try + { + // ... work ... + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogError(ex, "Background service failed"); + } +} +``` + +**Fix:** Add try/catch in `ExecuteAsync()` for any `BackgroundService` that should not crash the host on failure. + +Source: https://github.com/dotnet/runtime/pull/124863 + +### TarWriter emits HardLink entries for hard-linked files (Preview 3) + +**Impact: Low.** When `TarWriter` archives a directory containing hard links, the same inode encountered more than once is now written as a `HardLink` entry pointing back to the first occurrence, rather than duplicating the file data. + +**Fix:** If consuming tar archives produced by .NET code, ensure the reader handles `HardLink` entry types. + +Source: https://github.com/dotnet/runtime/pull/123874 + +### Zstandard APIs moved from preview package to System.IO.Compression (Preview 3) + +**Impact: Low.** `ZstandardStream` and related APIs that were previously in the `System.IO.Compression.Zstandard` preview NuGet package are now in-box in `System.IO.Compression`. + +**Fix:** Remove the `` preview package if present. The APIs are now available without any additional package reference. + +Source: https://github.com/dotnet/runtime/pull/114545 diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md index 9f88243..6ed0516 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/cryptography-dotnet10to11.md @@ -26,3 +26,14 @@ var signature = ecdsa.SignData(data, HashAlgorithmName.SHA256); - **Ed25519** — if available in your scenario This change only affects macOS. DSA continues to work on Windows and Linux (though it is generally considered a legacy algorithm). + +### AIA certificate downloads disabled by default during client-certificate validation (Preview 3) + +**Impact: Medium.** AIA (Authority Information Access) certificate downloads are now disabled by default when performing server-side client-certificate chain validation. Previously the runtime would attempt to fetch intermediate CA certificates online. + +**Fix:** If using mTLS where client certificates rely on AIA URLs for intermediate CAs, either: +- Pre-install the full certificate chain on the server +- Have clients send the full chain including intermediates +- Re-enable AIA downloads via `X509ChainPolicy.DisableCertificateDownloads = false` + +Source: https://github.com/dotnet/runtime/pull/125049 diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md index 4c417d2..d2e9cc5 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/efcore-dotnet10to11.md @@ -2,7 +2,7 @@ These breaking changes affect projects using Entity Framework Core 11. Source: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-11.0/breaking-changes -> **Note:** .NET 11 is in preview. The changes below were introduced in **Preview 1**. Additional EF Core breaking changes are expected in later previews. +> **Note:** .NET 11 is in preview. The changes below were introduced in **Preview 1 through Preview 3**. Additional EF Core breaking changes are expected in later previews. ## Medium-Impact Changes @@ -36,3 +36,69 @@ await context.SaveChangesAsync(); - `Any()` → `await AnyAsync()` Tracking issue: https://github.com/dotnet/efcore/issues/37059 + +### Cosmos: empty owned collections return empty collection instead of null (Preview 1) + +**Impact: Low.** When a Cosmos-backed entity has an owned collection with no items, the property now returns an empty collection rather than `null`. + +**Fix:** Update null checks to empty-collection checks: `if (entity.Items is null)` → `if (entity.Items.Count == 0)`. + +Tracking issue: https://github.com/dotnet/efcore/issues/36577 + +## Preview 3 Changes + +### RelationalEventId.MigrationsNotFound now throws by default (Preview 3) + +**Impact: Low.** Calling `Migrate()` or `MigrateAsync()` when no migrations exist in the assembly now throws an exception rather than silently logging. + +**Fix:** If intentional, suppress with: `options.ConfigureWarnings(w => w.Ignore(RelationalEventId.MigrationsNotFound))`. + +Source: https://github.com/dotnet/efcore/pull/37839 + +### EF Core Tools and Tasks no longer transitively depend on Design (Preview 3) + +**Impact: Low.** The `Microsoft.EntityFrameworkCore.Tools` and `Microsoft.EntityFrameworkCore.Tasks` NuGet packages no longer have a transitive dependency on `Microsoft.EntityFrameworkCore.Design`. + +**Fix:** If your project relied on this transitive reference, add it explicitly: + +```xml + +``` + +Source: https://github.com/dotnet/efcore/pull/37837 + +### EFOptimizeContext MSBuild property removed (Preview 3) + +**Impact: Low.** The `true` MSBuild property no longer exists. Code generation is now controlled by `` and ``. + +**Fix:** Replace `` with the two new properties. With `PublishAOT=true`, generation is automatic during publish. + +Source: https://github.com/dotnet/efcore/pull/37838 + +### SqlVector<T> properties excluded from SELECT by default (Preview 3) + +**Impact: Low.** `SqlVector` properties are now excluded from `SELECT` statements when materializing entities (they return `null`). They can still be used in `WHERE`/`ORDER BY` for vector search. + +**Fix:** Use explicit projections to include vector values: `.Select(b => new { b.Id, b.Embedding })`. + +Source: https://github.com/dotnet/efcore/pull/37829 + +### Microsoft.Data.SqlClient updated to 7.0 (Preview 3) + +**Impact: Medium.** EF Core's SQL Server provider now depends on `Microsoft.Data.SqlClient` 7.0. In v7, Azure/Entra ID authentication dependencies (`Azure.Core`, `Azure.Identity`, `Microsoft.Identity.Client`) have been removed from the core package. + +**Fix:** If using Entra ID authentication (e.g., `ActiveDirectoryDefault`, `ActiveDirectoryManagedIdentity`), add: + +```xml + +``` + +Source: https://github.com/dotnet/efcore/pull/37949 + +### Encryption-enabled SQLite packages removed (Preview 3) + +**Impact: Medium.** `SQLitePCLRaw 3.0` (used by `Microsoft.Data.Sqlite` 11) removed `bundle_e_sqlcipher` and several other bundle packages. + +**Fix:** Switch to SQLite Encryption Extension (SEE), SQLCipher from Zetetic, or `SQLite3MultipleCiphers-NuGet`. + +Source: https://github.com/dotnet/efcore/issues/37059 diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md index a290592..b2ded5f 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/runtime-jit-dotnet10to11.md @@ -49,3 +49,11 @@ For ReadyToRun-capable assemblies, there may be additional startup overhead on s **Fix:** Verify all deployment targets meet the new minimum requirements. For x86/x64, any CPU from ~2013 or later should be fine. For Windows Arm64, ensure `LSE` support (all Windows 11 compatible Arm64 devices). Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/jit/11/minimum-hardware-requirements + +### NativeAOT native-library outputs use `lib` prefix on Unix (Preview 3) + +**Impact: Low.** NativeAOT shared/native library outputs on Linux and macOS now follow Unix conventions and include the `lib` prefix (e.g., `libMyLib.so` instead of `MyLib.so`). + +**Fix:** Update build scripts, deployment pipelines, or P/Invoke declarations that reference output filenames by the old name without the `lib` prefix. + +Source: https://github.com/dotnet/runtime/pull/124611 diff --git a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md index 16377e7..e256ea6 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-upgrade/skills/migrate-dotnet10-to-dotnet11/references/sdk-msbuild-dotnet10to11.md @@ -11,3 +11,19 @@ These changes affect the .NET SDK, CLI tooling, NuGet, and MSBuild behavior. Sou **Impact: Low.** The mono launch target is no longer set automatically for .NET Framework apps. If you require Mono for execution on Linux, you need to specify it explicitly in the configuration. Source: https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/11/mono-launch-target-removed + +### NETSDK1235 warning for PackAsTool with custom .nuspec (Preview 2) + +**Impact: Low.** A new build warning `NETSDK1235` is emitted when a project has both `PackAsTool=true` and a custom `NuspecFile` property, which violates .NET Tool packaging requirements. Projects with `TreatWarningsAsErrors=true` will fail. + +**Fix:** Remove the custom `NuspecFile` property when packaging as a .NET Tool, or suppress the warning if the .nuspec is compatible. + +Source: https://github.com/dotnet/sdk/pull/52810 + +### `dotnet publish --self-contained` now parses the passed value (Preview 3) + +**Impact: Low.** `dotnet publish --self-contained` previously always interpreted the flag as `true` regardless of the passed value. It now correctly parses the value (e.g., `--self-contained false` actually produces a framework-dependent publish). + +**Fix:** Review build scripts that pass `--self-contained` to ensure the intended value is correct. + +Source: https://github.com/dotnet/sdk/pull/52333 diff --git a/external-sources/upstreams/dotnet-skills/dotnet/lsp.json b/external-sources/upstreams/dotnet-skills/dotnet/lsp.json index 38259e2..91c9b4f 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet/lsp.json +++ b/external-sources/upstreams/dotnet-skills/dotnet/lsp.json @@ -12,8 +12,11 @@ "--autoLoadProjects" ], "fileExtensions": { - ".cs": "csharp" - } + ".cs": "csharp", + ".razor": "aspnetcorerazor", + ".cshtml": "aspnetcorerazor" + }, + "warmupTimeoutMs": 120000 } } } diff --git a/external-sources/upstreams/dotnet-skills/dotnet/skills/csharp-scripts/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet/skills/csharp-scripts/SKILL.md index dad39fe..8f5c41b 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet/skills/csharp-scripts/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet/skills/csharp-scripts/SKILL.md @@ -1,41 +1,44 @@ --- name: csharp-scripts -description: Run single-file C# programs as scripts (file-based apps) for quick experimentation, prototyping, and concept testing. Use when the user wants to write and execute a small C# program without creating a full project. +description: "Run file-based C# apps with the .NET CLI when the user explicitly wants C#/.NET code without creating a project. Use for C# language/API experiments, one-file C# apps, small multi-file C# apps composed with `#:include`/`#:exclude`, or C# file-based apps linked with `#:ref`. Do not use for language-agnostic throwaway scripts, generic computations, Python/PowerShell-style automation, full projects, or existing app integration." license: MIT --- -# C# Scripts +# File-Based C# Apps ## When to Use -- Testing a C# concept, API, or language feature with a quick one-file program +- Testing a C# concept, API, or language feature with a quick file-based app - Prototyping logic before integrating it into a larger project +- Building a small utility from one entry-point file and a few helper `.cs` files ## When Not to Use -- The user needs a full project with multiple files or project references +- The user asks for a language-agnostic quick script, throwaway computation, or shell/Python/PowerShell-style automation +- The user needs a full project, solution integration, or project references in an existing app - The user is working inside an existing .NET solution and wants to add code there -- The program is too large or complex for a single file +- The app is large enough that project structure, build customization, tests, or publish configuration should live in a `.csproj` ## Inputs | Input | Required | Description | |-------|----------|-------------| -| C# code or intent | Yes | The code to run, or a description of what the script should do | +| C# code or intent | Yes | The code to run, or a description of what the file-based app should do | ## Workflow ### Step 1: Check the .NET SDK version -Run `dotnet --version` to verify the SDK is installed and note the major version number. File-based apps require .NET 10 or later. If the version is below 10, follow the [fallback for older SDKs](#fallback-for-net-9-and-earlier) instead. +Run `dotnet --version` to verify the SDK is installed and note the full version, including the feature band. File-based apps require .NET 10 or later. `#:include`, `#:exclude`, and transitive directive processing require SDK 10.0.300 or later; SDK 10.0.100/10.0.200 builds can run single-file apps but do not support those multi-file directives. If the version is below 10, follow the [fallback for older SDKs](#fallback-for-net-9-and-earlier) instead. -### Step 2: Write the script file +### Step 2: Write the app file -Create a single `.cs` file using top-level statements. Place it outside any existing project directory to avoid conflicts with `.csproj` files. +Create an entry-point `.cs` file using top-level statements. Place it outside any existing project directory to avoid conflicts with `.csproj` files. ```csharp +#!/usr/bin/env dotnet // hello.cs -Console.WriteLine("Hello from a C# script!"); +Console.WriteLine("Hello from a file-based app!"); var numbers = new[] { 1, 2, 3, 4, 5 }; Console.WriteLine($"Sum: {numbers.Sum()}"); @@ -47,7 +50,7 @@ Guidelines: - Place `using` directives at the top of the file (after the `#!` line and any `#:` directives if present) - Place type declarations (classes, records, enums) after all top-level statements -### Step 3: Run the script +### Step 3: Run the app ```bash dotnet hello.cs @@ -65,7 +68,7 @@ Place directives at the top of the file (immediately after an optional shebang l #### `#:package` — NuGet package references -Always specify a version: +Specify a version unless the app intentionally uses central package management. Use `@*` when the latest available package is acceptable (or `@*-*` for pre-release): ```csharp #:package Humanizer@2.14.1 @@ -109,6 +112,26 @@ Reference another project by relative path: #:project ../MyLibrary/MyLibrary.csproj ``` +#### `#:ref` — File-based app references + +Reference another `.cs` file as a separate file-based app project when it should compile into a separate assembly instead of being included in the same compilation. Use `#:include` for ordinary helper files that should share the same assembly as the entry point; use `#:ref` when you want project-reference-like boundaries. + +```csharp +#:property ExperimentalFileBasedProgramEnableRefDirective=true +#:ref ../Shared/Formatter.cs + +Console.WriteLine(Formatter.Title("hello world")); +``` + +Guidelines: + +- The referenced file is compiled as its own virtual project and added as a project reference. +- If the referenced file is a library without top-level statements, put `#:property OutputType=Library` in that referenced file. +- Members that must be consumed by the referencing app should be public; internal members are not visible across the assembly boundary. +- `#:ref` is transitive: a referenced file can contain its own `#:ref` and other `#:` directives. +- Relative paths are resolved relative to the file containing the directive. +- Some SDK builds require `#:property ExperimentalFileBasedProgramEnableRefDirective=true`; remove that property if the SDK accepts `#:ref` without it. + #### `#:sdk` — SDK selection Override the default SDK (`Microsoft.NET.Sdk`): @@ -117,9 +140,65 @@ Override the default SDK (`Microsoft.NET.Sdk`): #:sdk Microsoft.NET.Sdk.Web ``` +#### `#:include` and `#:exclude` — Multi-file apps + +In .NET SDK 10.0.300 and later, file-based apps can include additional files in the same virtual project. Check the full `dotnet --version` output before using these directives; a 10.0.100 or 10.0.200 SDK is still .NET 10 but does not support them. Use `#:include` for helper source files and supported assets, and `#:exclude` to remove files from an include pattern or default item set. + +```csharp +#!/usr/bin/env dotnet +#:include Helpers.cs +#:include Models/*.cs +#:exclude Models/Generated/*.cs + +Console.WriteLine(Formatter.Title("hello world")); +``` + +Guidelines: + +- Treat the file passed to `dotnet` as the entry point; put top-level statements there. +- Put declarations such as classes, records, and enums in included `.cs` files. +- Prefer explicit globs such as `Helpers.cs` or `Models/*.cs` over broad recursive globs. +- Paths are resolved relative to the file containing the directive. +- Include directives from non-entry-point C# files are processed too, so a helper file can declare its own `#:package`, `#:property`, `#:sdk`, `#:project`, `#:ref`, `#:include`, or `#:exclude` directives. +- Avoid duplicate directives across included files unless the directive kind explicitly supports duplicates; duplicate `#:package`, `#:property`, `#:sdk`, `#:include`, and `#:exclude` entries can fail. +- When an app uses `#:include`, add a shebang (`#!/usr/bin/env dotnet`) to the entry-point file on Unix-like systems to make the entry point clear to tools. Use `LF` line endings and no BOM for shebang files. + +Example layout: + +```text +scratch/ + hello.cs + Helpers.cs + Models/ + Person.cs +``` + +```csharp +#!/usr/bin/env dotnet +// hello.cs +#:include Helpers.cs +#:include Models/*.cs + +var person = new Person("Ada"); +Console.WriteLine(Formatter.Title(person.Name)); +``` + +```csharp +// Helpers.cs +static class Formatter +{ + public static string Title(string value) => value.ToUpperInvariant(); +} +``` + +```csharp +// Models/Person.cs +record Person(string Name); +``` + ### Step 5: Clean up -Remove the script file when the user is done. To clear cached build artifacts: +Remove the app files when the user is done. To clear cached build artifacts: ```bash dotnet clean hello.cs @@ -173,7 +252,7 @@ partial class AppJsonContext : JsonSerializerContext; ## Converting to a project -When a script outgrows a single file, convert it to a full project: +When a file-based app outgrows this workflow, convert it to a full project: ```bash dotnet project convert hello.cs @@ -184,29 +263,35 @@ dotnet project convert hello.cs If the .NET SDK version is below 10, file-based apps are not available. Use a temporary console project instead: ```bash -mkdir -p /tmp/csharp-script && cd /tmp/csharp-script +mkdir -p /tmp/csharp-file-based-app && cd /tmp/csharp-file-based-app dotnet new console -o . --force ``` -Replace the generated `Program.cs` with the script content and run with `dotnet run`. Add NuGet packages with `dotnet add package `. Remove the directory when done. +Replace the generated `Program.cs` with the app content and run with `dotnet run`. Add NuGet packages with `dotnet add package `. Remove the directory when done. ## Validation - [ ] `dotnet --version` reports 10.0 or later (or fallback path is used) -- [ ] The script compiles without errors (can be checked explicitly with `dotnet build .cs`) +- [ ] If the app uses `#:include`, `#:exclude`, or transitive directives from included files, `dotnet --version` reports SDK 10.0.300 or later +- [ ] The app compiles without errors (can be checked explicitly with `dotnet build .cs`) - [ ] `dotnet .cs` produces the expected output -- [ ] Script file and cached artifacts are cleaned up after the session +- [ ] Multi-file apps include every required helper file and exclude unintended matches +- [ ] App files and cached artifacts are cleaned up after the session ## Common Pitfalls | Pitfall | Solution | |---------|----------| -| `.cs` file is inside a directory with a `.csproj` | Move the script outside the project directory, or use `dotnet run --file file.cs` | +| `.cs` file is inside a directory with a `.csproj` | Move the app outside the project directory, or use `dotnet run --file file.cs` | | `#:package` without a version | Specify a version: `#:package PackageName@1.2.3` or `@*` for latest | | `#:property` with wrong syntax | Use `PropertyName=Value` with no spaces around `=` and no quotes: `#:property AllowUnsafeBlocks=true` | | Directives placed after C# code | All `#:` directives must appear immediately after an optional shebang line (if present) and before any `using` directives or other C# statements | +| Helper file is not compiled | Add `#:include Helper.cs` or an appropriate glob to the entry-point file | +| Shared file needs an assembly boundary | Use `#:ref Shared.cs` instead of `#:include Shared.cs`, and set `#:property OutputType=Library` in the referenced file if it has no entry point | +| Broad include pulls in unrelated files | Prefer narrow include patterns and use `#:exclude` for generated, backup, or experimental files | +| Duplicate directives in included files | Keep package, property, SDK, include, and exclude directives unique across the entry point and included C# files | | Reflection-based JSON serialization fails | Use source-generated JSON with `JsonSerializerContext` (see [Source-generated JSON](#source-generated-json)) | -| Unexpected build behavior or version errors | File-based apps inherit `global.json`, `Directory.Build.props`, `Directory.Build.targets`, and `nuget.config` from parent directories. Move the script to an isolated directory if the inherited settings conflict | +| Unexpected build behavior or version errors | File-based apps inherit `global.json`, `Directory.Build.props`, `Directory.Build.targets`, and `nuget.config` from parent directories. Move the app to an isolated directory if the inherited settings conflict | ## More info diff --git a/external-sources/vendir.lock.yml b/external-sources/vendir.lock.yml index 1deb6c5..70c2fc7 100644 --- a/external-sources/vendir.lock.yml +++ b/external-sources/vendir.lock.yml @@ -2,11 +2,10 @@ apiVersion: vendir.k14s.io/v1alpha1 directories: - contents: - git: - commitTitle: Bump the github-actions-dependencies group across 1 directory with - 3 updates (#631)... - sha: 4a4c424ee43f60354230389e96c39053307121d8 + commitTitle: Update binlog mcp name (#684) + sha: 0724ccf8ac485af4b6bf95b2489886cc33f170a3 tags: - - skill-validator-nightly-4-g4a4c424 + - skill-validator-nightly path: dotnet-skills path: upstreams kind: LockConfig diff --git a/tests/ManagedCode.DotnetSkills.Tests/CatalogOrganizationTests.cs b/tests/ManagedCode.DotnetSkills.Tests/CatalogOrganizationTests.cs index bbd5144..84736f9 100644 --- a/tests/ManagedCode.DotnetSkills.Tests/CatalogOrganizationTests.cs +++ b/tests/ManagedCode.DotnetSkills.Tests/CatalogOrganizationTests.cs @@ -26,7 +26,7 @@ public void Load_AssignsExpectedStacksAndLanes() AssertSkill(catalog, "xunit", "Testing", "Frameworks"); AssertSkill(catalog, "code-testing-agent", "Testing Research", "Automation"); AssertSkill(catalog, "stryker", "Testing Research", "Mutation"); - AssertSkill(catalog, "exp-test-gap-analysis", "Testing Research", "Experimental"); + AssertSkill(catalog, "test-gap-analysis", "Testing Research", "Experimental"); AssertSkill(catalog, "csharp-scripts", ".NET Foundations", "Tooling"); AssertSkill(catalog, "dotnet-pinvoke", ".NET Foundations", "Interop"); AssertSkill(catalog, "msbuild-modernization", "MSBuild", "Build Pipelines"); diff --git a/tests/ManagedCode.DotnetSkills.Tests/SkillInstallerTests.cs b/tests/ManagedCode.DotnetSkills.Tests/SkillInstallerTests.cs index 9be3ce5..d06f589 100644 --- a/tests/ManagedCode.DotnetSkills.Tests/SkillInstallerTests.cs +++ b/tests/ManagedCode.DotnetSkills.Tests/SkillInstallerTests.cs @@ -161,7 +161,7 @@ public void SelectSkillsFromCollections_ResolvesTestingResearchAliases_WithoutPu Assert.Contains(selected, skill => skill.Name == "code-testing-agent"); Assert.Contains(selected, skill => skill.Name == "stryker"); - Assert.Contains(selected, skill => skill.Name == "exp-test-gap-analysis"); + Assert.Contains(selected, skill => skill.Name == "test-gap-analysis"); Assert.DoesNotContain(selected, skill => skill.Name == "xunit"); Assert.DoesNotContain(selected, skill => skill.Name == "coverlet"); Assert.Equal(selected.Select(skill => skill.Name).Distinct(StringComparer.OrdinalIgnoreCase).Count(), selected.Count);