From c5c61723a21249784afa86384e2df8155078c888 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 2 Aug 2026 16:47:31 +0200 Subject: [PATCH 01/16] =?UTF-8?q?=E2=9C=A8=20introduce=20dotnet-test=20ski?= =?UTF-8?q?ll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive xUnit migration and testing skill for Codebelt conventions. Includes role-specific patterns for ordinary unit tests, ASP.NET Core functional tests, and console/worker functional tests. Provides deterministic project inspection, xUnit v3 modernization guidance, WebApplicationFactory elimination, package version resolution, and bootstrapper host patterns (MinimalConsoleProgram, MinimalWorkerProgram, MinimalWebProgram). --- skills/dotnet-test/FORMS.md | 66 ++++ skills/dotnet-test/SKILL.md | 156 ++++++++++ .../application/FocusedApplicationTest.cs | 27 ++ .../application/SharedApplicationTest.cs | 27 ++ .../bootstrapper/console-minimal/Program.cs | 20 ++ .../assets/bootstrapper/console/Program.cs | 13 + .../assets/bootstrapper/console/Startup.cs | 29 ++ .../bootstrapper/web-minimal/Program.cs | 17 ++ .../bootstrapper/worker-minimal/Program.cs | 16 + .../assets/bootstrapper/worker/Program.cs | 13 + .../assets/bootstrapper/worker/Startup.cs | 20 ++ .../assets/bootstrapper/worker/Worker.cs | 14 + .../dotnet-test/assets/unit/BehaviorTest.cs | 22 ++ .../assets/web/FocusedWebApplicationTest.cs | 28 ++ .../assets/web/SharedWebApplicationTest.cs | 29 ++ skills/dotnet-test/evals/evals.json | 114 +++++++ .../files/focused-web/Directory.Build.props | 13 + .../focused-web/Directory.Packages.props | 10 + .../Acme.Cdn.Origin/Acme.Cdn.Origin.csproj | 2 + .../src/Acme.Cdn.Origin/Program.cs | 7 + .../Acme.Cdn.Origin.FunctionalTests.csproj | 11 + .../CdnOriginTestApplication.cs | 32 ++ .../CompressionTest.cs | 25 ++ .../TempContent.cs | 18 ++ .../files/fresh-unit/Directory.Build.props | 9 + .../files/fresh-unit/Directory.Packages.props | 6 + .../Acme.Calculator/Acme.Calculator.csproj | 2 + .../src/Acme.Calculator/Calculator.cs | 10 + .../Acme.Calculator.Tests.csproj | 9 + .../files/shared-web/Directory.Build.props | 5 + .../files/shared-web/Directory.Packages.props | 10 + .../src/Acme.Status/Acme.Status.csproj | 2 + .../shared-web/src/Acme.Status/Program.cs | 9 + .../Acme.Status.FunctionalTests.csproj | 11 + .../Acme.Status.FunctionalTests/StatusTest.cs | 36 +++ .../v2-modernization/Directory.Build.props | 9 + .../v2-modernization/Directory.Packages.props | 11 + .../src/Acme.Inventory/Acme.Inventory.csproj | 2 + .../src/Acme.Inventory/InventoryService.cs | 10 + .../Acme.Inventory.Tests.csproj | 13 + .../InventoryServiceTest.cs | 26 ++ .../worker-functional/Directory.Build.props | 4 + .../Directory.Packages.props | 5 + .../src/Acme.QueuePump/Acme.QueuePump.csproj | 5 + .../src/Acme.QueuePump/Program.cs | 13 + .../src/Acme.QueuePump/QueuePumpMarker.cs | 4 + .../src/Acme.QueuePump/QueuePumpWorker.cs | 12 + .../src/Acme.QueuePump/Startup.cs | 20 ++ .../Acme.QueuePump.FunctionalTests.csproj | 5 + .../application-functional-tests.md | 40 +++ .../references/bootstrapper-hosts.md | 35 +++ .../references/migration-invariants.md | 35 +++ skills/dotnet-test/references/unit-tests.md | 36 +++ .../references/web-functional-tests.md | 51 ++++ .../references/xunit-v3-modernization.md | 22 ++ .../scripts/inspect-dotnet-tests.ps1 | 283 ++++++++++++++++++ .../scripts/resolve-test-package-versions.ps1 | 121 ++++++++ .../scripts/test-inspect-dotnet-tests.ps1 | 90 ++++++ skills/dotnet-test/scripts/validate-skill.ps1 | 31 ++ 59 files changed, 1721 insertions(+) create mode 100644 skills/dotnet-test/FORMS.md create mode 100644 skills/dotnet-test/SKILL.md create mode 100644 skills/dotnet-test/assets/application/FocusedApplicationTest.cs create mode 100644 skills/dotnet-test/assets/application/SharedApplicationTest.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/console-minimal/Program.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/console/Program.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/console/Startup.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/web-minimal/Program.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/worker-minimal/Program.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/worker/Program.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/worker/Startup.cs create mode 100644 skills/dotnet-test/assets/bootstrapper/worker/Worker.cs create mode 100644 skills/dotnet-test/assets/unit/BehaviorTest.cs create mode 100644 skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs create mode 100644 skills/dotnet-test/assets/web/SharedWebApplicationTest.cs create mode 100644 skills/dotnet-test/evals/evals.json create mode 100644 skills/dotnet-test/evals/files/focused-web/Directory.Build.props create mode 100644 skills/dotnet-test/evals/files/focused-web/Directory.Packages.props create mode 100644 skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Acme.Cdn.Origin.csproj create mode 100644 skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Program.cs create mode 100644 skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/Acme.Cdn.Origin.FunctionalTests.csproj create mode 100644 skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CdnOriginTestApplication.cs create mode 100644 skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CompressionTest.cs create mode 100644 skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/TempContent.cs create mode 100644 skills/dotnet-test/evals/files/fresh-unit/Directory.Build.props create mode 100644 skills/dotnet-test/evals/files/fresh-unit/Directory.Packages.props create mode 100644 skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Acme.Calculator.csproj create mode 100644 skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Calculator.cs create mode 100644 skills/dotnet-test/evals/files/fresh-unit/test/Acme.Calculator.Tests/Acme.Calculator.Tests.csproj create mode 100644 skills/dotnet-test/evals/files/shared-web/Directory.Build.props create mode 100644 skills/dotnet-test/evals/files/shared-web/Directory.Packages.props create mode 100644 skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Acme.Status.csproj create mode 100644 skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Program.cs create mode 100644 skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/Acme.Status.FunctionalTests.csproj create mode 100644 skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/StatusTest.cs create mode 100644 skills/dotnet-test/evals/files/v2-modernization/Directory.Build.props create mode 100644 skills/dotnet-test/evals/files/v2-modernization/Directory.Packages.props create mode 100644 skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/Acme.Inventory.csproj create mode 100644 skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/InventoryService.cs create mode 100644 skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/Acme.Inventory.Tests.csproj create mode 100644 skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/InventoryServiceTest.cs create mode 100644 skills/dotnet-test/evals/files/worker-functional/Directory.Build.props create mode 100644 skills/dotnet-test/evals/files/worker-functional/Directory.Packages.props create mode 100644 skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Acme.QueuePump.csproj create mode 100644 skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Program.cs create mode 100644 skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpMarker.cs create mode 100644 skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs create mode 100644 skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Startup.cs create mode 100644 skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj create mode 100644 skills/dotnet-test/references/application-functional-tests.md create mode 100644 skills/dotnet-test/references/bootstrapper-hosts.md create mode 100644 skills/dotnet-test/references/migration-invariants.md create mode 100644 skills/dotnet-test/references/unit-tests.md create mode 100644 skills/dotnet-test/references/web-functional-tests.md create mode 100644 skills/dotnet-test/references/xunit-v3-modernization.md create mode 100644 skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 create mode 100644 skills/dotnet-test/scripts/resolve-test-package-versions.ps1 create mode 100644 skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 create mode 100644 skills/dotnet-test/scripts/validate-skill.ps1 diff --git a/skills/dotnet-test/FORMS.md b/skills/dotnet-test/FORMS.md new file mode 100644 index 0000000..5ab16fa --- /dev/null +++ b/skills/dotnet-test/FORMS.md @@ -0,0 +1,66 @@ +# .NET Test Input Form + +Collect only unresolved fields. Prefer native structured controls when the host provides them. Otherwise use the plain-text fallback below without changing field order or defaults. + +## Fields + +### project_selection + +- **type:** single-choice +- **prompt:** Which test project should be bootstrapped or refactored? +- **choices:** Dynamically list discovered test `.csproj` files relative to the repository root +- **default:** The only discovered test project, or the project explicitly named by the user (Recommended) +- **required:** true + +### operation_mode + +- **type:** single-choice +- **prompt:** Should the selected project be bootstrapped or refactored? +- **choices:** + - Refactor an existing test project (Recommended when the project exists and contains tests) + - Bootstrap test coverage (Recommended when no selected test project exists or it has no behavior tests) +- **default:** Compute from the selected project +- **required:** true + +### test_role + +- **type:** single-choice +- **prompt:** Which test role should the selected project use? +- **choices:** + - Auto-classify from repository evidence (Recommended) + - Ordinary unit test + - ASP.NET Core functional test + - Console or worker functional test +- **default:** Auto-classify from repository evidence (Recommended) +- **required:** true + +### application_adaptation + +- **type:** single-choice +- **prompt:** If a console or worker executable has no Generic Host seam, may the application bootstrap be adapted? +- **choices:** + - Test code only; report the required application adaptation (Recommended) + - Application and test code are both in scope +- **default:** Test code only; report the required application adaptation (Recommended) +- **required:** true +- **show_when:** `test_role` is `Console or worker functional test`, or auto-classification reports a missing Generic Host blocker + +### confirmation + +- **type:** single-choice +- **prompt:** Apply the summarized project, mode, role, package-owner, and application-scope plan? +- **choices:** + - Yes (Recommended) + - No +- **default:** Yes (Recommended) +- **required:** true + +## Presentation rules + +- Infer explicit answers from the request and inspection output; do not ask them again. +- Ask one unresolved field at a time. +- Present the recommended/default choice first and suffix it with `(Recommended)`. +- In plain-text fallback mode, start immediately with `Field: ` and show numbered choices. Do not add a conversational preamble. +- If the user leaves a shown computed/default choice blank, accept it and continue. +- After all fields are resolved, summarize the exact project, mode, role, package owner, detected blockers, and application adaptation scope, then ask `confirmation`. + diff --git a/skills/dotnet-test/SKILL.md b/skills/dotnet-test/SKILL.md new file mode 100644 index 0000000..3801d9a --- /dev/null +++ b/skills/dotnet-test/SKILL.md @@ -0,0 +1,156 @@ +--- +name: dotnet-test +description: > + Bootstrap or refactor .NET xUnit test projects to Codebelt conventions. Use for unit-test setup, xUnit v2-to-v3 modernization, Microsoft Testing Platform adoption, ASP.NET Core WebApplicationFactory migration, shared web fixtures, and in-process console or worker functional tests. Classify the selected project, preserve existing behavior and test names, resolve compatible stable packages from NuGet, and validate restore/build/test. Do not use for NUnit/MSTest-only work, general production refactoring without a test-project goal, or process-launching end-to-end harnesses. +compatibility: > + Requires .NET SDK, PowerShell 7+, and network access to NuGet for dynamic package resolution. +--- + +# .NET Test + +Bootstrap and refactor xUnit projects using the tested patterns from [Codebelt xUnit](https://github.com/codebeltnet/xunit) and the matching application-host patterns from [Codebelt Bootstrapper](https://github.com/codebeltnet/bootstrapper). + +## Critical + +- Inspect before editing. Run `scripts/inspect-dotnet-tests.ps1` against the selected project and treat its role, package ownership, `WebApplicationFactory` inventory, and blockers as the starting contract. +- Classify every selected project as exactly one of: **Ordinary unit test**, **ASP.NET Core functional test**, or **Console or worker functional test**. +- Preserve target frameworks, central package management, unrelated MSBuild configuration, existing test names, and test isolation. +- Replace every selected `WebApplicationFactory` usage. A partial migration that leaves a selected usage or package reference behind is incomplete. +- Keep functional testing in-process. Never add a process-launching fallback for console or worker applications. +- If a selected executable has no Generic Host, adapt production startup only when application adaptation is explicitly in scope. Otherwise report the exact missing host seam and stop before changing production startup. +- During bootstrap, add at least one test derived from real source behavior. Placeholder assertions such as `Assert.True(true)` do not satisfy the task. +- When the request requires restore/build/test, `dotnet test` must discover the expected non-zero test count and report zero failures. An MTP executable run may supplement that gate but never replaces it; if `dotnet test` discovers zero tests, add or restore the repository-appropriate `xunit.runner.visualstudio` adapter and rerun. + +## Step 1: Resolve scope and inputs + +Read `FORMS.md`. Infer fields already answered by the request or repository. Ask only for unresolved fields, one at a time, and confirm the final summary before mutation. + +Resolve the repository root and selected `.csproj` path. Do not broaden a single-project request to every test project. + +Run: + +```powershell +pwsh -NoProfile -File "/scripts/inspect-dotnet-tests.ps1" -RepoRoot "" -ProjectPath "" +``` + +Keep stdout as JSON. Treat a non-zero exit or a reported blocker as a real stop condition. + +## Step 2: Classify the project + +Use the inspection evidence, then read only the matching role reference: + +| Role | Evidence | Required reference | +|---|---|---| +| Ordinary unit test | No application entry-point hosting boundary is exercised | `references/unit-tests.md` | +| ASP.NET Core functional test | HTTP pipeline, `WebApplicationFactory`, TestHost, or an ASP.NET Core entry point is exercised | `references/web-functional-tests.md` | +| Console or worker functional test | A Generic Host console/worker entry point or hosted service is exercised without an ASP.NET Core HTTP pipeline | `references/application-functional-tests.md` | + +If the evidence conflicts with the requested role, report the conflict and ask before applying a materially different pattern. + +## Step 3: Resolve packages without hardcoding latest + +Run the resolver for the selected target frameworks and role: + +```powershell +pwsh -NoProfile -File "/scripts/resolve-test-package-versions.ps1" -TargetFramework -Role +``` + +The resolver queries NuGet stable versions and verifies candidate compatibility through an isolated restore. If it fails, report the package, target frameworks, and restore evidence instead of guessing. + +Preserve package ownership: + +- Central Package Management: update or add `PackageVersion` in the owning `Directory.Packages.props`; keep project `PackageReference` items versionless. +- Project-owned versions: update only the selected `.csproj` unless the user expands scope. +- Imported/shared ownership: edit the actual owning props file only when it is inside the authorized scope; otherwise report the required owner change. + +Read `references/xunit-v3-modernization.md` whenever inspection reports xUnit v2 or Microsoft Testing Platform is not active. + +## Step 4: Apply the role pattern + +### Ordinary unit tests + +- Inherit `Test` or the repository's established `Test`-derived base. +- Accept `ITestOutputHelper output` and pass it to the base constructor. +- Keep the SUT namespace; use file-scoped namespaces for new files. +- Preserve existing test method names during refactoring. +- Name new tests `Should{Expected}_When{Condition}`. + +### Focused ASP.NET Core functional tests + +- Keep the test class derived from `Test`. +- Use `WebApplicationTestFactory.Create(...)` per focused test or per deliberately owned test scope. +- Create the client from `application.Host.GetTestClient()` or the returned `TestServer` as appropriate. +- Dispose the factory result, clients, responses, and owned external resources at the same effective lifecycle as before. + +### Shared ASP.NET Core fixtures + +- Derive the test class from `WebApplicationTest>`, or from an established derived fixture type that preserves the same contract. +- Accept the fixture and `ITestOutputHelper` in the constructor and pass both to the base. +- Put shared host customization in `ConfigureWebHost` or a narrowly derived fixture when configuration must exist before the first host start. + +### Focused console or worker functional tests + +- Keep the test class derived from `Test`. +- Use `ApplicationTestFactory.Create(...)` and inspect services/configuration through the returned host test. + +### Shared console or worker fixtures + +- Derive from `ApplicationTest>`, or from an established derived fixture type with the same lifecycle. +- Accept the fixture and `ITestOutputHelper` in the constructor and pass both to the base. +- Put host customization in `ConfigureHost`. + +For fresh console or worker applications, read `references/bootstrapper-hosts.md` and adapt the matching assets. Do not substitute a vanilla process runner. + +Preserve an existing Bootstrapper host family. `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` are valid Generic Host seams; do not convert them to their Startup-based counterparts merely to enable tests. + +## Step 5: Migrate behavior, not just types + +For any `WebApplicationFactory` migration, read `references/migration-invariants.md` before editing. Inventory and preserve: + +- host and application configuration; +- environment selection; +- service replacement and registration order; +- lazy-start or first-client behavior; +- client options, base address, handlers, and cookies; +- direct host, server, services, and configuration access; +- sync and async disposal; +- temporary files, ports, databases, and other isolation boundaries. + +Delete `Microsoft.AspNetCore.Mvc.Testing` only when no selected code or remaining authorized project surface needs it. After edits, search the selected scope for both `WebApplicationFactory` and `Microsoft.AspNetCore.Mvc.Testing`. + +## Step 6: Bootstrap a behavior test + +Read the selected production source, its public behavior, and nearby tests. Choose the lowest-cost deterministic behavior that could catch a real defect. Adapt the matching asset rather than copying it literally: + +- `assets/unit/BehaviorTest.cs` +- `assets/web/FocusedWebApplicationTest.cs` +- `assets/web/SharedWebApplicationTest.cs` +- `assets/application/FocusedApplicationTest.cs` +- `assets/application/SharedApplicationTest.cs` + +Replace every placeholder with repository evidence. Do not invent an endpoint, service, configuration key, or expected result. + +## Step 7: Validate and loop + +Run the narrowest authoritative sequence that covers the selected change: + +1. rerun `inspect-dotnet-tests.ps1`; +2. restore the selected test project; +3. build the selected test project; +4. run `dotnet test` when restore/build/test was requested and confirm the expected non-zero test count with zero failures; a zero-discovery exit code is a failure and `dotnet run` is not a substitute; +5. for migrations, search the selected scope and confirm zero remaining `WebApplicationFactory` usages; +6. inspect the final diff for target-framework, package-owner, test-name, and unrelated-change drift. + +If tests expose a migration regression, repair the preserved lifecycle or configuration behavior rather than weakening assertions. + +## Completion report + +Report: + +- selected project and classified role; +- mode and whether production application adaptation was in scope; +- package ownership and resolved versions; +- preserved migration invariants; +- behavior test added or existing tests retained; +- exact restore/build/test and zero-usage-search results; +- blockers or validation limits. diff --git a/skills/dotnet-test/assets/application/FocusedApplicationTest.cs b/skills/dotnet-test/assets/application/FocusedApplicationTest.cs new file mode 100644 index 0000000..c64d342 --- /dev/null +++ b/skills/dotnet-test/assets/application/FocusedApplicationTest.cs @@ -0,0 +1,27 @@ +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace {APPLICATION_NAMESPACE}; + +public class {BEHAVIOR}Test : Test +{ + public {BEHAVIOR}Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Should{EXPECTED}_When{CONDITION}() + { + using var application = ApplicationTestFactory.Create<{ENTRY_POINT}>(builder => + { + {PRESERVED_HOST_CONFIGURATION} + }); + + var actual = application.Host.Services.GetRequiredService<{SOURCE_GROUNDED_SERVICE}>(); + + Assert.Equal({SOURCE_GROUNDED_EXPECTED}, actual.{SOURCE_GROUNDED_MEMBER}); + } +} + diff --git a/skills/dotnet-test/assets/application/SharedApplicationTest.cs b/skills/dotnet-test/assets/application/SharedApplicationTest.cs new file mode 100644 index 0000000..5151cb7 --- /dev/null +++ b/skills/dotnet-test/assets/application/SharedApplicationTest.cs @@ -0,0 +1,27 @@ +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace {APPLICATION_NAMESPACE}; + +public class {BEHAVIOR}Test : ApplicationTest<{ENTRY_POINT}, BlockingManagedApplicationFixture<{ENTRY_POINT}>> +{ + public {BEHAVIOR}Test(BlockingManagedApplicationFixture<{ENTRY_POINT}> hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void Should{EXPECTED}_When{CONDITION}() + { + var actual = Host.Services.GetRequiredService<{SOURCE_GROUNDED_SERVICE}>(); + + Assert.Equal({SOURCE_GROUNDED_EXPECTED}, actual.{SOURCE_GROUNDED_MEMBER}); + } + + protected override void ConfigureHost(IHostBuilder builder) + { + {PRESERVED_SHARED_HOST_CONFIGURATION} + } +} + diff --git a/skills/dotnet-test/assets/bootstrapper/console-minimal/Program.cs b/skills/dotnet-test/assets/bootstrapper/console-minimal/Program.cs new file mode 100644 index 0000000..e28026a --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/console-minimal/Program.cs @@ -0,0 +1,20 @@ +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Program : MinimalConsoleProgram +{ + public static Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + {PRESERVED_SERVICE_REGISTRATIONS} + return builder.Build().RunAsync(); + } + + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + return {APPLICATION_RUN_TASK}; + } +} diff --git a/skills/dotnet-test/assets/bootstrapper/console/Program.cs b/skills/dotnet-test/assets/bootstrapper/console/Program.cs new file mode 100644 index 0000000..5dd5140 --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/console/Program.cs @@ -0,0 +1,13 @@ +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Program : ConsoleProgram +{ + public static Task Main(string[] args) + { + return CreateHostBuilder(args).Build().RunAsync(); + } +} + diff --git a/skills/dotnet-test/assets/bootstrapper/console/Startup.cs b/skills/dotnet-test/assets/bootstrapper/console/Startup.cs new file mode 100644 index 0000000..40fcbdc --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/console/Startup.cs @@ -0,0 +1,29 @@ +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Startup : ConsoleStartup +{ + public Startup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + {PRESERVED_SERVICE_REGISTRATIONS} + } + + public override void ConfigureConsole(IServiceProvider serviceProvider) + { + {PRESERVED_CONSOLE_CONFIGURATION} + } + + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + return {APPLICATION_RUN_TASK}; + } +} + diff --git a/skills/dotnet-test/assets/bootstrapper/web-minimal/Program.cs b/skills/dotnet-test/assets/bootstrapper/web-minimal/Program.cs new file mode 100644 index 0000000..9f41b92 --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/web-minimal/Program.cs @@ -0,0 +1,17 @@ +using Codebelt.Bootstrapper.Web; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Program : MinimalWebProgram +{ + public static Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + {PRESERVED_SERVICE_REGISTRATIONS} + + var app = builder.Build(); + {PRESERVED_WEB_PIPELINE_AND_ENDPOINTS} + return app.RunAsync(); + } +} diff --git a/skills/dotnet-test/assets/bootstrapper/worker-minimal/Program.cs b/skills/dotnet-test/assets/bootstrapper/worker-minimal/Program.cs new file mode 100644 index 0000000..3e4786f --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/worker-minimal/Program.cs @@ -0,0 +1,16 @@ +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Program : MinimalWorkerProgram +{ + public static Task Main(string[] args) + { + var builder = CreateHostBuilder(args); + builder.Services.AddHostedService<{WORKER_TYPE}>(); + {PRESERVED_SERVICE_REGISTRATIONS} + return builder.Build().RunAsync(); + } +} diff --git a/skills/dotnet-test/assets/bootstrapper/worker/Program.cs b/skills/dotnet-test/assets/bootstrapper/worker/Program.cs new file mode 100644 index 0000000..c3c6677 --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/worker/Program.cs @@ -0,0 +1,13 @@ +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Program : WorkerProgram +{ + public static Task Main(string[] args) + { + return CreateHostBuilder(args).Build().RunAsync(); + } +} + diff --git a/skills/dotnet-test/assets/bootstrapper/worker/Startup.cs b/skills/dotnet-test/assets/bootstrapper/worker/Startup.cs new file mode 100644 index 0000000..e06ff2a --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/worker/Startup.cs @@ -0,0 +1,20 @@ +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Startup : WorkerStartup +{ + public Startup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddHostedService(); + {PRESERVED_SERVICE_REGISTRATIONS} + } +} + diff --git a/skills/dotnet-test/assets/bootstrapper/worker/Worker.cs b/skills/dotnet-test/assets/bootstrapper/worker/Worker.cs new file mode 100644 index 0000000..620c58b --- /dev/null +++ b/skills/dotnet-test/assets/bootstrapper/worker/Worker.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace {APPLICATION_NAMESPACE}; + +public sealed class Worker(ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + {SOURCE_GROUNDED_WORKER_BEHAVIOR} + await Task.CompletedTask.ConfigureAwait(false); + } +} + diff --git a/skills/dotnet-test/assets/unit/BehaviorTest.cs b/skills/dotnet-test/assets/unit/BehaviorTest.cs new file mode 100644 index 0000000..a96f6a7 --- /dev/null +++ b/skills/dotnet-test/assets/unit/BehaviorTest.cs @@ -0,0 +1,22 @@ +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace {SUT_NAMESPACE}; + +public class {SUT_TYPE}Test : Test +{ + public {SUT_TYPE}Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Should{EXPECTED}_When{CONDITION}() + { + var sut = {SOURCE_GROUNDED_ARRANGE}; + + var actual = {SOURCE_GROUNDED_ACT}; + + Assert.Equal({SOURCE_GROUNDED_EXPECTED}, actual); + } +} + diff --git a/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs b/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs new file mode 100644 index 0000000..ced10d9 --- /dev/null +++ b/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs @@ -0,0 +1,28 @@ +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace {APPLICATION_NAMESPACE}; + +public class {BEHAVIOR}Test : Test +{ + public {BEHAVIOR}Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task Should{EXPECTED}_When{CONDITION}() + { + using var application = WebApplicationTestFactory.Create<{ENTRY_POINT}>(builder => + { + {PRESERVED_WEB_HOST_CONFIGURATION} + }); + using var client = application.Host.GetTestClient(); + + using var response = await client.GetAsync("{SOURCE_GROUNDED_ROUTE}").ConfigureAwait(false); + + Assert.Equal({SOURCE_GROUNDED_STATUS}, response.StatusCode); + } +} + diff --git a/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs b/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs new file mode 100644 index 0000000..b72b51a --- /dev/null +++ b/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs @@ -0,0 +1,29 @@ +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace {APPLICATION_NAMESPACE}; + +public class {BEHAVIOR}Test : WebApplicationTest<{ENTRY_POINT}, BlockingManagedWebApplicationFixture<{ENTRY_POINT}>> +{ + public {BEHAVIOR}Test(BlockingManagedWebApplicationFixture<{ENTRY_POINT}> hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public async Task Should{EXPECTED}_When{CONDITION}() + { + using var client = Host.GetTestClient(); + + using var response = await client.GetAsync("{SOURCE_GROUNDED_ROUTE}").ConfigureAwait(false); + + Assert.Equal({SOURCE_GROUNDED_STATUS}, response.StatusCode); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + {PRESERVED_SHARED_WEB_HOST_CONFIGURATION} + } +} + diff --git a/skills/dotnet-test/evals/evals.json b/skills/dotnet-test/evals/evals.json new file mode 100644 index 0000000..694e1db --- /dev/null +++ b/skills/dotnet-test/evals/evals.json @@ -0,0 +1,114 @@ +{ + "skill_name": "dotnet-test", + "evals": [ + { + "id": 1, + "prompt": "In the attached Acme.Calculator fixture, bootstrap the existing test project as an ordinary Codebelt xUnit test project. Add a real behavior test for Calculator.Add, preserve net10.0 and central package management, use xUnit v3 with Microsoft Testing Platform, and run restore/build/test.", + "expected_output": "A buildable xUnit v3/MTP unit-test project with Codebelt Test inheritance, ITestOutputHelper, and a source-grounded Add behavior test.", + "expectations": [ + "Classifies the selected project as an ordinary unit test", + "Keeps net10.0 and central package management", + "Uses xunit.v3, Microsoft Testing Platform, and a compatible Codebelt xUnit package", + "Adds a Test-derived class with ITestOutputHelper and file-scoped Acme.Calculator namespace", + "Adds a ShouldReturnSum_WhenAddingTwoNumbers-style source-grounded behavior test rather than a placeholder", + "Restore, build, and test succeed" + ], + "files": [ + "evals/files/fresh-unit/Directory.Build.props", + "evals/files/fresh-unit/Directory.Packages.props", + "evals/files/fresh-unit/src/Acme.Calculator/Acme.Calculator.csproj", + "evals/files/fresh-unit/src/Acme.Calculator/Calculator.cs", + "evals/files/fresh-unit/test/Acme.Calculator.Tests/Acme.Calculator.Tests.csproj" + ] + }, + { + "id": 2, + "prompt": "Modernize the attached Acme.Inventory.Tests xUnit v2 project to xUnit v3 and Microsoft Testing Platform using Codebelt conventions. Preserve the target framework, central package ownership, existing test class and method names, and unrelated project settings. Run restore/build/test.", + "expected_output": "The existing tests compile and pass on xUnit v3/MTP with preserved names and central versions.", + "expectations": [ + "Detects xUnit v2 and central package ownership", + "Replaces xunit and Xunit.Abstractions with the xUnit v3 shape", + "Enables executable output and UseMicrosoftTestingPlatformRunner without changing net10.0", + "Makes InventoryServiceTest inherit Test and inject ITestOutputHelper", + "Preserves InventoryServiceTest and GetAvailableCount_ReturnsOnlyAvailableItems names", + "Restore, build, and test succeed" + ], + "files": [ + "evals/files/v2-modernization/Directory.Build.props", + "evals/files/v2-modernization/Directory.Packages.props", + "evals/files/v2-modernization/src/Acme.Inventory/Acme.Inventory.csproj", + "evals/files/v2-modernization/src/Acme.Inventory/InventoryService.cs", + "evals/files/v2-modernization/test/Acme.Inventory.Tests/Acme.Inventory.Tests.csproj", + "evals/files/v2-modernization/test/Acme.Inventory.Tests/InventoryServiceTest.cs" + ] + }, + { + "id": 3, + "prompt": "Migrate the attached lean Acme.Cdn.Origin functional tests from the web-cdn-origin-style CdnOriginTestApplication WebApplicationFactory wrapper to the focused Codebelt WebApplicationTestFactory pattern. Preserve per-test settings, Production environment, temporary content ownership, client behavior, services, disposal, isolation, and existing test names. Remove every selected WebApplicationFactory usage and validate restore/build/test.", + "expected_output": "Focused Test-derived functional tests use WebApplicationTestFactory with no WebApplicationFactory remaining and retain per-test isolation/configuration.", + "expectations": [ + "Classifies the project as ASP.NET Core functional tests and selects focused ownership", + "Replaces CdnOriginTestApplication and all WebApplicationFactory usages with WebApplicationTestFactory", + "Preserves Production environment, in-memory settings, temporary content disposal, and one application per test", + "Keeps CompressionTest and both existing method names unchanged", + "Removes Microsoft.AspNetCore.Mvc.Testing when no longer needed", + "Search finds no WebApplicationFactory in the selected migration and restore/build/test succeed" + ], + "files": [ + "evals/files/focused-web/Directory.Build.props", + "evals/files/focused-web/Directory.Packages.props", + "evals/files/focused-web/src/Acme.Cdn.Origin/Acme.Cdn.Origin.csproj", + "evals/files/focused-web/src/Acme.Cdn.Origin/Program.cs", + "evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/Acme.Cdn.Origin.FunctionalTests.csproj", + "evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CdnOriginTestApplication.cs", + "evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CompressionTest.cs", + "evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/TempContent.cs" + ] + }, + { + "id": 4, + "prompt": "Migrate the attached Acme.Status functional-test class from IClassFixture> to the shared Codebelt WebApplicationTest> pattern. Preserve shared fixture lifecycle, Staging environment, configuration, service access, client behavior, and existing test names. Remove every selected WebApplicationFactory usage and run restore/build/test.", + "expected_output": "A shared blocking managed web fixture replaces IClassFixture WebApplicationFactory with preserved host customization and passing tests.", + "expectations": [ + "Selects the shared fixture pattern rather than focused per-test ownership", + "Uses WebApplicationTest with BlockingManagedWebApplicationFixture and constructor-injected ITestOutputHelper", + "Moves Staging/configuration customization into the pre-start shared host customization path", + "Preserves StatusTest and its existing test method names", + "Removes Microsoft.AspNetCore.Mvc.Testing when unused", + "Search finds no WebApplicationFactory and restore/build/test succeed" + ], + "files": [ + "evals/files/shared-web/Directory.Build.props", + "evals/files/shared-web/Directory.Packages.props", + "evals/files/shared-web/src/Acme.Status/Acme.Status.csproj", + "evals/files/shared-web/src/Acme.Status/Program.cs", + "evals/files/shared-web/test/Acme.Status.FunctionalTests/Acme.Status.FunctionalTests.csproj", + "evals/files/shared-web/test/Acme.Status.FunctionalTests/StatusTest.cs" + ] + }, + { + "id": 5, + "prompt": "Bootstrap the attached Acme.QueuePump worker functional-test project using the focused Codebelt ApplicationTestFactory pattern. The application already follows Codebelt Bootstrapper Worker and must stay in-process; do not launch a process. Add a source-grounded test that resolves QueuePumpMarker from the started host, preserve net10.0 and central package management, and run restore/build/test.", + "expected_output": "A buildable focused console/worker functional test uses ApplicationTestFactory against the existing Bootstrapper Generic Host and verifies a real registered service.", + "expectations": [ + "Classifies the project as a console or worker functional test", + "Recognizes the existing Codebelt Bootstrapper Worker Generic Host and does not rewrite production startup unnecessarily", + "Uses a Test-derived class with ITestOutputHelper and ApplicationTestFactory", + "Does not use Process.Start, dotnet run, shell execution, or port polling", + "Adds a ShouldResolveMarker_WhenApplicationStarts-style test grounded in QueuePumpMarker registration", + "Restore, build, and test succeed" + ], + "files": [ + "evals/files/worker-functional/Directory.Build.props", + "evals/files/worker-functional/Directory.Packages.props", + "evals/files/worker-functional/src/Acme.QueuePump/Acme.QueuePump.csproj", + "evals/files/worker-functional/src/Acme.QueuePump/Program.cs", + "evals/files/worker-functional/src/Acme.QueuePump/Startup.cs", + "evals/files/worker-functional/src/Acme.QueuePump/QueuePumpMarker.cs", + "evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs", + "evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj" + ] + } + ] +} + diff --git a/skills/dotnet-test/evals/files/focused-web/Directory.Build.props b/skills/dotnet-test/evals/files/focused-web/Directory.Build.props new file mode 100644 index 0000000..380c35c --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/Directory.Build.props @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + $(MSBuildProjectName.EndsWith('Tests')) + + + Exe + true + + + diff --git a/skills/dotnet-test/evals/files/focused-web/Directory.Packages.props b/skills/dotnet-test/evals/files/focused-web/Directory.Packages.props new file mode 100644 index 0000000..bd580fc --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/Directory.Packages.props @@ -0,0 +1,10 @@ + + true + + + + + + + + diff --git a/skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Acme.Cdn.Origin.csproj b/skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Acme.Cdn.Origin.csproj new file mode 100644 index 0000000..dd2327a --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Acme.Cdn.Origin.csproj @@ -0,0 +1,2 @@ + + diff --git a/skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Program.cs b/skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Program.cs new file mode 100644 index 0000000..93ef5de --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/src/Acme.Cdn.Origin/Program.cs @@ -0,0 +1,7 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.MapGet("/compression", (IConfiguration configuration) => configuration.GetValue("Compression:Enabled", false) ? "br" : "identity"); +app.Run(); + +public partial class Program; + diff --git a/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/Acme.Cdn.Origin.FunctionalTests.csproj b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/Acme.Cdn.Origin.FunctionalTests.csproj new file mode 100644 index 0000000..32f4530 --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/Acme.Cdn.Origin.FunctionalTests.csproj @@ -0,0 +1,11 @@ + + Acme.Cdn.Origin + + + + + + + + + diff --git a/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CdnOriginTestApplication.cs b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CdnOriginTestApplication.cs new file mode 100644 index 0000000..2dc36c8 --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CdnOriginTestApplication.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Acme.Cdn.Origin; + +public sealed class CdnOriginTestApplication : WebApplicationFactory +{ + private readonly Dictionary _settings; + + public CdnOriginTestApplication(IDictionary? settings = null) + { + Content = new TempContent(); + _settings = settings is null ? new() : new(settings); + } + + public TempContent Content { get; } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment(Environments.Production); + builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(_settings)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) { Content.Dispose(); } + } +} + diff --git a/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CompressionTest.cs b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CompressionTest.cs new file mode 100644 index 0000000..a7193fb --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/CompressionTest.cs @@ -0,0 +1,25 @@ +using Xunit; + +namespace Acme.Cdn.Origin; + +public class CompressionTest +{ + [Fact] + public async Task Get_ShouldNotCompress_WhenCompressionDisabled() + { + await using var application = new CdnOriginTestApplication(); + using var client = application.CreateClient(); + + Assert.Equal("identity", await client.GetStringAsync("/compression")); + } + + [Fact] + public async Task Get_ShouldCompress_WhenEnabled() + { + await using var application = new CdnOriginTestApplication(new Dictionary { ["Compression:Enabled"] = "true" }); + using var client = application.CreateClient(); + + Assert.Equal("br", await client.GetStringAsync("/compression")); + } +} + diff --git a/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/TempContent.cs b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/TempContent.cs new file mode 100644 index 0000000..c70d5fc --- /dev/null +++ b/skills/dotnet-test/evals/files/focused-web/test/Acme.Cdn.Origin.FunctionalTests/TempContent.cs @@ -0,0 +1,18 @@ +namespace Acme.Cdn.Origin; + +public sealed class TempContent : IDisposable +{ + public TempContent() + { + Root = Path.Combine(Path.GetTempPath(), "acme-cdn-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Root); + } + + public string Root { get; } + + public void Dispose() + { + if (Directory.Exists(Root)) { Directory.Delete(Root, true); } + } +} + diff --git a/skills/dotnet-test/evals/files/fresh-unit/Directory.Build.props b/skills/dotnet-test/evals/files/fresh-unit/Directory.Build.props new file mode 100644 index 0000000..e37888f --- /dev/null +++ b/skills/dotnet-test/evals/files/fresh-unit/Directory.Build.props @@ -0,0 +1,9 @@ + + + net10.0 + enable + enable + $(MSBuildProjectName.EndsWith('Tests')) + + + diff --git a/skills/dotnet-test/evals/files/fresh-unit/Directory.Packages.props b/skills/dotnet-test/evals/files/fresh-unit/Directory.Packages.props new file mode 100644 index 0000000..d13cce6 --- /dev/null +++ b/skills/dotnet-test/evals/files/fresh-unit/Directory.Packages.props @@ -0,0 +1,6 @@ + + + true + + + diff --git a/skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Acme.Calculator.csproj b/skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Acme.Calculator.csproj new file mode 100644 index 0000000..9609c61 --- /dev/null +++ b/skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Acme.Calculator.csproj @@ -0,0 +1,2 @@ + + diff --git a/skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Calculator.cs b/skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Calculator.cs new file mode 100644 index 0000000..e061358 --- /dev/null +++ b/skills/dotnet-test/evals/files/fresh-unit/src/Acme.Calculator/Calculator.cs @@ -0,0 +1,10 @@ +namespace Acme.Calculator; + +public static class Calculator +{ + public static int Add(int left, int right) + { + return left + right; + } +} + diff --git a/skills/dotnet-test/evals/files/fresh-unit/test/Acme.Calculator.Tests/Acme.Calculator.Tests.csproj b/skills/dotnet-test/evals/files/fresh-unit/test/Acme.Calculator.Tests/Acme.Calculator.Tests.csproj new file mode 100644 index 0000000..2cf4b13 --- /dev/null +++ b/skills/dotnet-test/evals/files/fresh-unit/test/Acme.Calculator.Tests/Acme.Calculator.Tests.csproj @@ -0,0 +1,9 @@ + + + Acme.Calculator + + + + + + diff --git a/skills/dotnet-test/evals/files/shared-web/Directory.Build.props b/skills/dotnet-test/evals/files/shared-web/Directory.Build.props new file mode 100644 index 0000000..c0cb788 --- /dev/null +++ b/skills/dotnet-test/evals/files/shared-web/Directory.Build.props @@ -0,0 +1,5 @@ + + net10.0enableenable$(MSBuildProjectName.EndsWith('Tests')) + Exetrue + + diff --git a/skills/dotnet-test/evals/files/shared-web/Directory.Packages.props b/skills/dotnet-test/evals/files/shared-web/Directory.Packages.props new file mode 100644 index 0000000..bd580fc --- /dev/null +++ b/skills/dotnet-test/evals/files/shared-web/Directory.Packages.props @@ -0,0 +1,10 @@ + + true + + + + + + + + diff --git a/skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Acme.Status.csproj b/skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Acme.Status.csproj new file mode 100644 index 0000000..dd2327a --- /dev/null +++ b/skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Acme.Status.csproj @@ -0,0 +1,2 @@ + + diff --git a/skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Program.cs b/skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Program.cs new file mode 100644 index 0000000..0440868 --- /dev/null +++ b/skills/dotnet-test/evals/files/shared-web/src/Acme.Status/Program.cs @@ -0,0 +1,9 @@ +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(new StatusMarker("ready")); +var app = builder.Build(); +app.MapGet("/status", (StatusMarker marker, IConfiguration configuration, IHostEnvironment environment) => $"{marker.Value}|{configuration["Status:Lane"]}|{environment.EnvironmentName}"); +app.Run(); + +public sealed record StatusMarker(string Value); +public partial class Program; + diff --git a/skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/Acme.Status.FunctionalTests.csproj b/skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/Acme.Status.FunctionalTests.csproj new file mode 100644 index 0000000..32007f0 --- /dev/null +++ b/skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/Acme.Status.FunctionalTests.csproj @@ -0,0 +1,11 @@ + + Acme.Status + + + + + + + + + diff --git a/skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/StatusTest.cs b/skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/StatusTest.cs new file mode 100644 index 0000000..50be8b9 --- /dev/null +++ b/skills/dotnet-test/evals/files/shared-web/test/Acme.Status.FunctionalTests/StatusTest.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Acme.Status; + +public class StatusTest : IClassFixture> +{ + private readonly WebApplicationFactory _application; + + public StatusTest(WebApplicationFactory application) + { + _application = application.WithWebHostBuilder(builder => + { + builder.UseEnvironment(Environments.Staging); + builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(new Dictionary { ["Status:Lane"] = "shared" })); + }); + } + + [Fact] + public async Task GetStatus_ShouldReturnConfiguredStatus() + { + using var client = _application.CreateClient(); + Assert.Equal("ready|shared|Staging", await client.GetStringAsync("/status")); + } + + [Fact] + public void Services_ShouldExposeStatusMarker() + { + Assert.Equal("ready", _application.Services.GetRequiredService().Value); + } +} + diff --git a/skills/dotnet-test/evals/files/v2-modernization/Directory.Build.props b/skills/dotnet-test/evals/files/v2-modernization/Directory.Build.props new file mode 100644 index 0000000..e37888f --- /dev/null +++ b/skills/dotnet-test/evals/files/v2-modernization/Directory.Build.props @@ -0,0 +1,9 @@ + + + net10.0 + enable + enable + $(MSBuildProjectName.EndsWith('Tests')) + + + diff --git a/skills/dotnet-test/evals/files/v2-modernization/Directory.Packages.props b/skills/dotnet-test/evals/files/v2-modernization/Directory.Packages.props new file mode 100644 index 0000000..19caf98 --- /dev/null +++ b/skills/dotnet-test/evals/files/v2-modernization/Directory.Packages.props @@ -0,0 +1,11 @@ + + + true + + + + + + + + diff --git a/skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/Acme.Inventory.csproj b/skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/Acme.Inventory.csproj new file mode 100644 index 0000000..9609c61 --- /dev/null +++ b/skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/Acme.Inventory.csproj @@ -0,0 +1,2 @@ + + diff --git a/skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/InventoryService.cs b/skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/InventoryService.cs new file mode 100644 index 0000000..fda21b5 --- /dev/null +++ b/skills/dotnet-test/evals/files/v2-modernization/src/Acme.Inventory/InventoryService.cs @@ -0,0 +1,10 @@ +namespace Acme.Inventory; + +public sealed class InventoryService +{ + public int GetAvailableCount(IEnumerable availability) + { + return availability.Count(value => value); + } +} + diff --git a/skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/Acme.Inventory.Tests.csproj b/skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/Acme.Inventory.Tests.csproj new file mode 100644 index 0000000..ed1a35f --- /dev/null +++ b/skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/Acme.Inventory.Tests.csproj @@ -0,0 +1,13 @@ + + + Acme.Inventory + false + + + + + + + + + diff --git a/skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/InventoryServiceTest.cs b/skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/InventoryServiceTest.cs new file mode 100644 index 0000000..d1e6b39 --- /dev/null +++ b/skills/dotnet-test/evals/files/v2-modernization/test/Acme.Inventory.Tests/InventoryServiceTest.cs @@ -0,0 +1,26 @@ +using Xunit; +using Xunit.Abstractions; + +namespace Acme.Inventory; + +public class InventoryServiceTest +{ + private readonly ITestOutputHelper _output; + + public InventoryServiceTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void GetAvailableCount_ReturnsOnlyAvailableItems() + { + var sut = new InventoryService(); + _output.WriteLine("Counting available items."); + + var actual = sut.GetAvailableCount(new[] { true, false, true }); + + Assert.Equal(2, actual); + } +} + diff --git a/skills/dotnet-test/evals/files/worker-functional/Directory.Build.props b/skills/dotnet-test/evals/files/worker-functional/Directory.Build.props new file mode 100644 index 0000000..9fd93be --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/Directory.Build.props @@ -0,0 +1,4 @@ + + net10.0enableenable$(MSBuildProjectName.EndsWith('Tests')) + + diff --git a/skills/dotnet-test/evals/files/worker-functional/Directory.Packages.props b/skills/dotnet-test/evals/files/worker-functional/Directory.Packages.props new file mode 100644 index 0000000..039ce95 --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/Directory.Packages.props @@ -0,0 +1,5 @@ + + true + + + diff --git a/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Acme.QueuePump.csproj b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Acme.QueuePump.csproj new file mode 100644 index 0000000..0382cdb --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Acme.QueuePump.csproj @@ -0,0 +1,5 @@ + + Exe + + + diff --git a/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Program.cs b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Program.cs new file mode 100644 index 0000000..42d599f --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Program.cs @@ -0,0 +1,13 @@ +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.Hosting; + +namespace Acme.QueuePump; + +public sealed class Program : WorkerProgram +{ + public static Task Main(string[] args) + { + return CreateHostBuilder(args).Build().RunAsync(); + } +} + diff --git a/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpMarker.cs b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpMarker.cs new file mode 100644 index 0000000..8e241f0 --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpMarker.cs @@ -0,0 +1,4 @@ +namespace Acme.QueuePump; + +public sealed record QueuePumpMarker(string Value); + diff --git a/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs new file mode 100644 index 0000000..6ef30ec --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Hosting; + +namespace Acme.QueuePump; + +public sealed class QueuePumpWorker : BackgroundService +{ + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return Task.CompletedTask; + } +} + diff --git a/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Startup.cs b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Startup.cs new file mode 100644 index 0000000..18578fb --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/src/Acme.QueuePump/Startup.cs @@ -0,0 +1,20 @@ +using Codebelt.Bootstrapper.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Acme.QueuePump; + +public sealed class Startup : WorkerStartup +{ + public Startup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(new QueuePumpMarker("queue-pump")); + services.AddHostedService(); + } +} + diff --git a/skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj b/skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj new file mode 100644 index 0000000..88f20f3 --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj @@ -0,0 +1,5 @@ + + Acme.QueuePump + + + diff --git a/skills/dotnet-test/references/application-functional-tests.md b/skills/dotnet-test/references/application-functional-tests.md new file mode 100644 index 0000000..2b8b100 --- /dev/null +++ b/skills/dotnet-test/references/application-functional-tests.md @@ -0,0 +1,40 @@ +# Console and worker functional tests + +Use Codebelt's Generic Host application-entry-point abstraction. The application must expose a host that `ApplicationHostFactory` can resolve in-process. + +## Focused factory ownership + +Keep the test derived from `Test` and use: + +```csharp +using var application = ApplicationTestFactory.Create(builder => +{ + builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(settings)); +}); + +var service = application.Host.Services.GetRequiredService(); +``` + +Use this when a test needs its own configured host or isolated state. + +## Shared fixture ownership + +Derive from: + +```csharp +ApplicationTest> +``` + +Pass the fixture and `ITestOutputHelper` to the base constructor. Override `ConfigureHost(IHostBuilder)` for configuration that must exist before the host starts. + +## Host seam gate + +Acceptable evidence includes Codebelt Bootstrapper `ConsoleProgram`, `MinimalConsoleProgram`, `WorkerProgram`, or `MinimalWorkerProgram`, or another entry point that builds an `IHost`/`IHostBuilder` discoverable by the Codebelt application host factory. + +Do not introduce `Process.Start`, `dotnet run`, shell execution, port polling, or redirected console-process management as a fallback. When only tests are in scope and the executable has no Generic Host, report: + +1. the current entry point and why no resolvable host exists; +2. the matching Codebelt Bootstrapper host base; +3. the production files that would need adaptation; +4. the test pattern that becomes available after adaptation. + diff --git a/skills/dotnet-test/references/bootstrapper-hosts.md b/skills/dotnet-test/references/bootstrapper-hosts.md new file mode 100644 index 0000000..d59d796 --- /dev/null +++ b/skills/dotnet-test/references/bootstrapper-hosts.md @@ -0,0 +1,35 @@ +# Codebelt Bootstrapper host patterns + +Use these patterns for fresh console/worker functional testing or when production bootstrap adaptation is explicitly authorized. Adapt the assets to repository conventions and behavior; asset files are literal examples with placeholders, not templates processed automatically. + +## Console + +- Startup model: `Program : ConsoleProgram` and `Startup : ConsoleStartup`. +- Minimal model: `Program : MinimalConsoleProgram` (or the established non-generic `MinimalConsoleProgram`) and override `RunAsync`. +- The entry point builds and runs the host returned by `CreateHostBuilder(args)`. + +Use `assets/bootstrapper/console/Program.cs` and `Startup.cs` for the Startup model. +Use `assets/bootstrapper/console-minimal/Program.cs` for the minimal model. Preserve the existing generic or non-generic base shape rather than changing it solely for tests. + +## Worker + +- Startup model: `Program : WorkerProgram` and `Startup : WorkerStartup`. +- Minimal model: `Program : MinimalWorkerProgram`, register hosted services on the builder, build, and run. + +Use `assets/bootstrapper/worker/Program.cs`, `Startup.cs`, and `Worker.cs` for the Startup model. +Use `assets/bootstrapper/worker-minimal/Program.cs` and adapt the existing worker registration for the minimal model. + +## Web + +- Startup model: preserve an established `WebProgram` and its existing web-startup pipeline. +- Minimal model: `Program : MinimalWebProgram`, configure the returned `WebApplicationBuilder`, build the application, map the existing pipeline/endpoints, and run it. + +Use `assets/bootstrapper/web-minimal/Program.cs` for the minimal model. A `MinimalWebProgram` application is still an ASP.NET Core functional-test role: use the focused or shared web pattern, not `ApplicationTestFactory`. + +## Pattern selection + +Treat `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` as complete Codebelt Generic Host seams. Do not rewrite them into Startup-based `ConsoleProgram`, `WorkerProgram`, or `WebProgram` applications merely to make tests possible. Likewise, do not convert an established Startup-based host to a minimal program as test cleanup. + +## Scope boundary + +Adopting a Bootstrapper NuGet package is a production application decision. Preserve an existing compatible Generic Host when it already satisfies Codebelt xUnit discovery. Add or change Bootstrapper packages only when application adaptation is explicitly in scope and the repository does not already establish another compatible Codebelt host pattern. diff --git a/skills/dotnet-test/references/migration-invariants.md b/skills/dotnet-test/references/migration-invariants.md new file mode 100644 index 0000000..75cf342 --- /dev/null +++ b/skills/dotnet-test/references/migration-invariants.md @@ -0,0 +1,35 @@ +# WebApplicationFactory migration invariants + +Before editing, create an inventory for every selected factory type and call site. + +## Host construction + +- environment name and content root; +- configuration sources, order, and key values; +- service additions, removals, replacement order, and scopes; +- TestServer configuration and any custom host builder behavior; +- whether host creation is lazy until `CreateClient`, `Server`, or `Services` is first used. + +## Client behavior + +- base address; +- redirect and cookie handling; +- default headers; +- custom handlers; +- one client per test versus shared clients. + +## Resource ownership + +- sync/async factory disposal; +- client and response disposal; +- temporary directories/files; +- database/container/message-broker fixtures; +- environment variables or static state; +- fixture/collection parallelization boundaries. + +## Selection rule + +Prefer focused `WebApplicationTestFactory` ownership when the old test constructed a factory per method, passed varying settings, or owned temporary resources per test. Prefer `WebApplicationTest<...>` when the old project used `IClassFixture>` and its shared lifecycle is intentional. + +After migration, search the authorized scope. Zero selected `WebApplicationFactory` identifiers is a completion gate, but it is not sufficient by itself: restore/build/test must also pass and lifecycle invariants must still hold. + diff --git a/skills/dotnet-test/references/unit-tests.md b/skills/dotnet-test/references/unit-tests.md new file mode 100644 index 0000000..44ee1f8 --- /dev/null +++ b/skills/dotnet-test/references/unit-tests.md @@ -0,0 +1,36 @@ +# Ordinary unit tests + +Use this role when the test exercises a type or collaboration without starting an application entry point. + +## Codebelt shape + +The behavioral source is `Codebelt.Extensions.Xunit.Test` and the Codebelt xUnit test suite: + +```csharp +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Acme.Product; + +public class WidgetTest : Test +{ + public WidgetTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ShouldReturnExpectedValue_WhenInputIsValid() + { + // Arrange, act, and assert observable behavior. + } +} +``` + +Preserve an established base class when it already derives from `Test` and carries real shared behavior. Do not flatten it merely to make every class inherit `Test` directly. + +## Bootstrap selection + +Read the selected production source and choose a deterministic public behavior. Prefer a small logic boundary over a slow application boundary. A generated test must fail for a plausible defect; placeholders and construction-only assertions do not qualify. + +Use the production namespace, not a `.Tests` suffix, when applying Codebelt conventions to new files. Preserve existing namespaces during a scoped modernization unless namespace migration is explicitly requested. + diff --git a/skills/dotnet-test/references/web-functional-tests.md b/skills/dotnet-test/references/web-functional-tests.md new file mode 100644 index 0000000..1b76a84 --- /dev/null +++ b/skills/dotnet-test/references/web-functional-tests.md @@ -0,0 +1,51 @@ +# ASP.NET Core functional tests + +Codebelt xUnit provides two application-entry-point patterns. Choose by ownership and sharing, not by test size. + +## Focused factory ownership + +Use `WebApplicationTestFactory` when a test or test method owns a separately configured application: + +```csharp +using var application = WebApplicationTestFactory.Create(builder => +{ + builder.UseEnvironment(Environments.Production); + builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(settings)); +}); +using var client = application.Host.GetTestClient(); +``` + +This maps naturally from tests that previously created a new `WebApplicationFactory` per method. Keep the test class derived from `Test`. + +## Shared xUnit fixture ownership + +Use `WebApplicationTest` when all tests in a class share one initialized host: + +```csharp +public class HealthTest : WebApplicationTest> +{ + public HealthTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment(Environments.Production); + } +} +``` + +`BlockingManagedWebApplicationFixture` starts the resolved application host synchronously so `TestServer` is ready after fixture initialization. Configuration must be established before first start; do not rely on per-test mutation of a shared host. + +## `WebApplicationFactory` mapping + +| Existing surface | Codebelt focused mapping | Codebelt shared mapping | +|---|---|---| +| `ConfigureWebHost` override | `WebApplicationTestFactory.Create` callback | test `ConfigureWebHost` override or derived blocking fixture | +| `CreateClient()` | `application.Host.GetTestClient()` | `Host.GetTestClient()` or `Server.CreateClient()` | +| `Services` | `application.Host.Services` | `Host.Services` / `Server.Services` | +| factory disposal | dispose returned host test | xUnit disposes the class fixture | +| per-test settings | callback plus per-test owned state | separate fixture type/collection or keep focused ownership | + +Do not force a shared fixture onto tests whose isolation depends on a fresh host or fresh temporary resource per method. + diff --git a/skills/dotnet-test/references/xunit-v3-modernization.md b/skills/dotnet-test/references/xunit-v3-modernization.md new file mode 100644 index 0000000..b231cfc --- /dev/null +++ b/skills/dotnet-test/references/xunit-v3-modernization.md @@ -0,0 +1,22 @@ +# xUnit v3 and Microsoft Testing Platform modernization + +Modernize the selected project without rewriting unrelated project infrastructure. + +## Required project shape + +- Replace xUnit v2 packages with `xunit.v3` and the repository's runner packages. +- Set test projects to executable output when not inherited: `Exe`. +- Enable Microsoft Testing Platform: `true`. +- Remove `Xunit.Abstractions`; import `Xunit` for `ITestOutputHelper`. +- Keep `Microsoft.NET.Test.Sdk` and `xunit.v3.runner.console`. Retain an established shared `xunit.runner.visualstudio` reference; otherwise add it with `PrivateAssets="all"` when the required `dotnet test` validation needs the adapter, matching the Codebelt xUnit project shape. +- Preserve coverage packages and their `PrivateAssets`/`IncludeAssets` metadata. + +## Package ownership + +Do not move versions between project and central files merely because the Codebelt source repo uses Central Package Management. Preserve the selected repository's ownership model. When central management is active, add versions to the owning `Directory.Packages.props` and keep project references versionless. + +## Source compatibility + +Preserve test names. Update only v2 API breaks, such as `using Xunit.Abstractions;`. Do not rewrite assertions or method names as modernization cleanup. + +Run restore, build, and test after the project-file change before doing optional source cleanup. A zero-discovery test command is not a successful test run: require the expected non-zero test count and zero failures. If `dotnet test` discovers no tests from an executable MTP project, add or restore the repository-appropriate adapter instead of reporting the MTP executable run as proof that the requested `dotnet test` gate passed. diff --git a/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 b/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 new file mode 100644 index 0000000..0238fdd --- /dev/null +++ b/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 @@ -0,0 +1,283 @@ +param( + [string]$RepoRoot = (Get-Location).Path, + [string]$ProjectPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) +[Console]::OutputEncoding = $utf8NoBom +$OutputEncoding = $utf8NoBom + +function Resolve-ContainedPath { + param([string]$Root, [string]$Path) + + $resolvedRoot = (Resolve-Path -LiteralPath $Root).Path.TrimEnd('\', '/') + $candidate = if ([System.IO.Path]::IsPathRooted($Path)) { $Path } else { Join-Path $resolvedRoot $Path } + $resolvedCandidate = (Resolve-Path -LiteralPath $candidate).Path + if (-not $resolvedCandidate.StartsWith($resolvedRoot + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase) -and + -not [string]::Equals($resolvedCandidate, $resolvedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Path '$Path' is outside repository root '$resolvedRoot'." + } + return $resolvedCandidate +} + +function Convert-ToRelativePath { + param([string]$Root, [string]$Path) + return [System.IO.Path]::GetRelativePath($Root, $Path).Replace('\', '/') +} + +function Get-EvaluatedProperties { + param([string]$Path) + + $arguments = @( + 'msbuild', $Path, '-nologo', + '-getProperty:TargetFramework,TargetFrameworks,IsTestProject,OutputType,ManagePackageVersionsCentrally,UseMicrosoftTestingPlatformRunner,RootNamespace' + ) + $output = @(& dotnet @arguments 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "dotnet msbuild property evaluation failed for '$Path' with exit code $LASTEXITCODE.`n$($output -join [Environment]::NewLine)" + } + try { + return (($output -join [Environment]::NewLine) | ConvertFrom-Json).Properties + } catch { + throw "dotnet msbuild did not return parseable JSON for '$Path'.`n$($output -join [Environment]::NewLine)" + } +} + +function Get-AncestorFiles { + param([string]$StartDirectory, [string]$Root, [string]$Name) + + $files = [System.Collections.Generic.List[string]]::new() + $current = [System.IO.DirectoryInfo]::new($StartDirectory) + $rootPath = [System.IO.Path]::GetFullPath($Root).TrimEnd('\', '/') + while ($null -ne $current) { + $candidate = Join-Path $current.FullName $Name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + $files.Add((Resolve-Path -LiteralPath $candidate).Path) + } + if ([string]::Equals($current.FullName.TrimEnd('\', '/'), $rootPath, [System.StringComparison]::OrdinalIgnoreCase)) { break } + $current = $current.Parent + } + return @($files) +} + +function Get-PackageNodes { + param([string]$Path, [string]$NodeName) + + try { [xml]$xml = [System.IO.File]::ReadAllText($Path, $utf8NoBom) } catch { return @() } + $nodes = @($xml.SelectNodes("//$NodeName")) + return @($nodes | ForEach-Object { + $versionNode = $_.SelectSingleNode('Version') + $versionAttribute = [string]$_.GetAttribute('Version') + $version = if ($null -ne $versionNode) { [string]$versionNode.InnerText } elseif (-not [string]::IsNullOrWhiteSpace($versionAttribute)) { $versionAttribute } else { $null } + [pscustomobject]@{ + id = [string]$_.GetAttribute('Include') + version = $version + owner = $Path + } + } | Where-Object { -not [string]::IsNullOrWhiteSpace($_.id) }) +} + +function Get-ProjectReferencePaths { + param([string]$Path) + + try { [xml]$xml = [System.IO.File]::ReadAllText($Path, $utf8NoBom) } catch { return @() } + $directory = Split-Path -Path $Path -Parent + return @($xml.SelectNodes('//ProjectReference') | ForEach-Object { + $include = [string]$_.GetAttribute('Include') + if ([string]::IsNullOrWhiteSpace($include)) { return } + $candidate = [System.IO.Path]::GetFullPath((Join-Path $directory $include)) + if (Test-Path -LiteralPath $candidate -PathType Leaf) { $candidate } + } | Sort-Object -Unique) +} + +function Get-SourceFiles { + param([string]$Directory) + return @(Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter '*.cs' | + Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } | + Sort-Object FullName) +} + +function Test-GenericHostEntryPoint { + param([string]$ProjectReference) + + $directory = Split-Path -Path $ProjectReference -Parent + $source = (Get-SourceFiles -Directory $directory | ForEach-Object { [System.IO.File]::ReadAllText($_.FullName, $utf8NoBom) }) -join "`n" + return $source -match 'Host\.Create(DefaultBuilder|ApplicationBuilder)|HostApplicationBuilder|WebApplication\.CreateBuilder|CreateHostBuilder\s*\(|:\s*(ConsoleProgram|MinimalConsoleProgram|WorkerProgram|MinimalWorkerProgram|WebProgram|MinimalWebProgram)' +} + +function Test-WebEntryPoint { + param([string]$ProjectReference) + + $projectText = [System.IO.File]::ReadAllText($ProjectReference, $utf8NoBom) + $directory = Split-Path -Path $ProjectReference -Parent + $source = (Get-SourceFiles -Directory $directory | ForEach-Object { [System.IO.File]::ReadAllText($_.FullName, $utf8NoBom) }) -join "`n" + return $projectText -match 'Microsoft\.NET\.Sdk\.Web' -or $source -match 'WebApplication\.CreateBuilder|:\s*(WebProgram|MinimalWebProgram)' +} + +function Get-HostPattern { + param([string]$ProjectReference) + + $directory = Split-Path -Path $ProjectReference -Parent + $source = (Get-SourceFiles -Directory $directory | ForEach-Object { [System.IO.File]::ReadAllText($_.FullName, $utf8NoBom) }) -join "`n" + foreach ($pattern in @('MinimalConsoleProgram', 'MinimalWorkerProgram', 'MinimalWebProgram', 'ConsoleProgram', 'WorkerProgram', 'WebProgram')) { + if ($source -match ":\s*$pattern(?:\s*<|\b)") { return $pattern } + } + if ($source -match 'WebApplication\.CreateBuilder') { return 'ASP.NET Core minimal host' } + if ($source -match 'Host\.Create(DefaultBuilder|ApplicationBuilder)|HostApplicationBuilder|CreateHostBuilder\s*\(') { return 'Generic Host' } + return $null +} + +$repoRootPath = (Resolve-Path -LiteralPath $RepoRoot).Path +$projects = if ([string]::IsNullOrWhiteSpace($ProjectPath)) { + @(Get-ChildItem -LiteralPath $repoRootPath -Recurse -File -Filter '*.csproj' | + Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } | + Sort-Object FullName | + Where-Object { + $text = [System.IO.File]::ReadAllText($_.FullName, $utf8NoBom) + $_.BaseName -match '(Tests?|FunctionalTests)$' -or $text -match '\s*true\s*|PackageReference[^>]+Include="(?:xunit|xunit\.v3)"' + } | + Select-Object -ExpandProperty FullName) +} else { + @((Resolve-ContainedPath -Root $repoRootPath -Path $ProjectPath)) +} + +if (@($projects).Count -eq 0) { throw "No test projects were found under '$repoRootPath'." } + +$reports = foreach ($project in $projects) { + if ([System.IO.Path]::GetExtension($project) -ne '.csproj') { throw "Selected project is not a .csproj: $project" } + $projectDirectory = Split-Path -Path $project -Parent + $properties = Get-EvaluatedProperties -Path $project + $sourceFiles = @(Get-SourceFiles -Directory $projectDirectory) + $sourceRecords = @($sourceFiles | ForEach-Object { + [pscustomobject]@{ + path = Convert-ToRelativePath -Root $repoRootPath -Path $_.FullName + lines = [System.IO.File]::ReadAllLines($_.FullName, $utf8NoBom) + text = [System.IO.File]::ReadAllText($_.FullName, $utf8NoBom) + } + }) + $combinedSource = (@($sourceRecords | ForEach-Object { $_.text }) -join "`n") + + $centralFiles = @(Get-AncestorFiles -StartDirectory $projectDirectory -Root $repoRootPath -Name 'Directory.Packages.props') + $buildPropsFiles = @(Get-AncestorFiles -StartDirectory $projectDirectory -Root $repoRootPath -Name 'Directory.Build.props') + $centralVersions = @{} + foreach ($centralFile in @($centralFiles | Sort-Object { $_.Length })) { + foreach ($node in Get-PackageNodes -Path $centralFile -NodeName 'PackageVersion') { + $centralVersions[$node.id] = $node + } + } + $packageReferences = [System.Collections.Generic.List[object]]::new() + foreach ($owner in @($project) + @($buildPropsFiles)) { + foreach ($node in Get-PackageNodes -Path $owner -NodeName 'PackageReference') { + $central = if ($centralVersions.ContainsKey($node.id)) { $centralVersions[$node.id] } else { $null } + $packageReferences.Add([pscustomobject]@{ + id = $node.id + version = if ($node.version) { $node.version } elseif ($central) { $central.version } else { $null } + referenceOwner = Convert-ToRelativePath -Root $repoRootPath -Path $owner + versionOwner = if ($node.version) { Convert-ToRelativePath -Root $repoRootPath -Path $owner } elseif ($central) { Convert-ToRelativePath -Root $repoRootPath -Path $central.owner } else { $null } + ownership = if ($node.version) { 'project-or-import' } elseif ($central) { 'central' } else { 'unresolved-or-transitive' } + }) + } + } + $packages = @($packageReferences | Sort-Object id, referenceOwner -Unique) + $packageIds = @($packages | ForEach-Object { $_.id }) + + $webUsages = [System.Collections.Generic.List[object]]::new() + $inheritance = [System.Collections.Generic.List[object]]::new() + foreach ($record in $sourceRecords) { + for ($index = 0; $index -lt $record.lines.Count; $index++) { + $line = $record.lines[$index] + if ($line -match '\bWebApplicationFactory(?:\s*<|\b)') { + $webUsages.Add([pscustomobject]@{ path = $record.path; line = $index + 1; text = $line.Trim() }) + } + if ($line -match '\bclass\s+(?[A-Za-z_][A-Za-z0-9_]*)[^:\r\n]*:\s*(?[^\{]+)') { + $inheritance.Add([pscustomobject]@{ path = $record.path; line = $index + 1; type = $Matches.name; baseTypes = $Matches.base.Trim() }) + } + } + } + + $projectReferences = @(Get-ProjectReferencePaths -Path $project) + $referencedHosts = @($projectReferences | ForEach-Object { + $referencedProjectText = [System.IO.File]::ReadAllText($_, $utf8NoBom) + [pscustomobject]@{ + path = Convert-ToRelativePath -Root $repoRootPath -Path $_ + genericHost = Test-GenericHostEntryPoint -ProjectReference $_ + webHost = Test-WebEntryPoint -ProjectReference $_ + hostPattern = Get-HostPattern -ProjectReference $_ + executable = $referencedProjectText -match '\s*Exe\s*|Microsoft\.NET\.Sdk\.(?:Worker|Web)' + } + }) + + $isWeb = $webUsages.Count -gt 0 -or + $packageIds -contains 'Microsoft.AspNetCore.Mvc.Testing' -or + $packageIds -contains 'Microsoft.AspNetCore.TestHost' -or + $packageIds -contains 'Codebelt.Extensions.Xunit.Hosting.AspNetCore' -or + $combinedSource -match '\b(WebApplicationTestFactory|WebApplicationTest<|TestServer)\b' -or + @($referencedHosts | Where-Object webHost).Count -gt 0 + $isApplication = -not $isWeb -and ( + $combinedSource -match '\b(ApplicationTestFactory|ApplicationTest<|BlockingManagedApplicationFixture)\b' -or + @($referencedHosts | Where-Object genericHost).Count -gt 0 -or + @($referencedHosts | Where-Object executable).Count -gt 0 -or + $combinedSource -match '\b(IHostedService|BackgroundService)\b' + ) + $role = if ($isWeb) { 'ASP.NET Core functional test' } elseif ($isApplication) { 'Console or worker functional test' } else { 'Ordinary unit test' } + + $xunitGeneration = if ($packageIds -contains 'xunit.v3' -or $combinedSource -match '\bXunit\.v3\b') { + 'v3' + } elseif ($packageIds -contains 'xunit' -or $packageIds -contains 'xunit.core' -or $combinedSource -match '\bXunit\.Abstractions\b') { + 'v2' + } else { + 'unknown' + } + + $frameworks = if (-not [string]::IsNullOrWhiteSpace([string]$properties.TargetFrameworks)) { + @(([string]$properties.TargetFrameworks).Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) + } elseif (-not [string]::IsNullOrWhiteSpace([string]$properties.TargetFramework)) { + @([string]$properties.TargetFramework) + } else { @() } + + $blockers = [System.Collections.Generic.List[string]]::new() + if ($role -eq 'Console or worker functional test' -and $referencedHosts.Count -gt 0 -and @($referencedHosts | Where-Object genericHost).Count -eq 0) { + $blockers.Add('The referenced executable does not expose a detectable Generic Host entry point. Test-only scope must report the required application-host adaptation; application scope must adapt bootstrap before using ApplicationTestFactory.') + } + if ($role -eq 'Console or worker functional test' -and $projectReferences.Count -eq 0 -and $combinedSource -notmatch '\b(ApplicationTestFactory|ApplicationTest<)\b') { + $blockers.Add('No referenced executable or existing Codebelt application-test entry point was found for the console/worker functional-test role.') + } + + $recommendations = [System.Collections.Generic.List[string]]::new() + if ($xunitGeneration -eq 'v2') { $recommendations.Add('Modernize the selected project to xUnit v3 and Microsoft Testing Platform while preserving target frameworks and package ownership.') } + if ([string]$properties.UseMicrosoftTestingPlatformRunner -ne 'true') { $recommendations.Add('Enable UseMicrosoftTestingPlatformRunner for the selected xUnit v3 test project, preferably in its existing shared test-project property owner.') } + if ($webUsages.Count -gt 0) { $recommendations.Add('Replace every selected WebApplicationFactory usage and preserve configuration, start behavior, clients, services, disposal, and isolation.') } + switch ($role) { + 'Ordinary unit test' { $recommendations.Add('Use Test or an established Test-derived base with ITestOutputHelper.') } + 'ASP.NET Core functional test' { $recommendations.Add('Use WebApplicationTestFactory for focused ownership or WebApplicationTest with BlockingManagedWebApplicationFixture for shared fixture ownership.') } + 'Console or worker functional test' { $recommendations.Add('Use ApplicationTestFactory for focused ownership or ApplicationTest with BlockingManagedApplicationFixture for shared fixture ownership.') } + } + + [pscustomobject]@{ + project = Convert-ToRelativePath -Root $repoRootPath -Path $project + role = $role + frameworks = @($frameworks) + xunitGeneration = $xunitGeneration + properties = [ordered]@{ + isTestProject = [string]$properties.IsTestProject + outputType = [string]$properties.OutputType + useMicrosoftTestingPlatformRunner = [string]$properties.UseMicrosoftTestingPlatformRunner + managePackageVersionsCentrally = [string]$properties.ManagePackageVersionsCentrally + rootNamespace = [string]$properties.RootNamespace + } + packageOwnership = $packages + inheritance = @($inheritance | Sort-Object path, line) + webApplicationFactoryUsages = @($webUsages | Sort-Object path, line) + referencedApplications = @($referencedHosts | Sort-Object path) + recommendations = @($recommendations) + blockers = @($blockers) + } +} + +[ordered]@{ + repoRoot = $repoRootPath + projectCount = @($reports).Count + projects = @($reports) +} | ConvertTo-Json -Depth 8 diff --git a/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 b/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 new file mode 100644 index 0000000..804a4bc --- /dev/null +++ b/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 @@ -0,0 +1,121 @@ +param( + [Parameter(Mandatory = $true)] + [string[]]$TargetFramework, + + [Parameter(Mandatory = $true)] + [ValidateSet('Unit', 'WebFunctional', 'ApplicationFunctional')] + [string]$Role, + + [string[]]$PackageId, + + [ValidateRange(1, 100)] + [int]$MaximumCandidates = 30 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) +[Console]::OutputEncoding = $utf8NoBom +$OutputEncoding = $utf8NoBom + +function Get-VersionKey { + param([string]$Version) + $parts = $Version.Split('.') + return [pscustomobject]@{ + major = if ($parts.Count -gt 0) { [int]$parts[0] } else { 0 } + minor = if ($parts.Count -gt 1) { [int]$parts[1] } else { 0 } + patch = if ($parts.Count -gt 2) { [int]$parts[2] } else { 0 } + revision = if ($parts.Count -gt 3) { [int]$parts[3] } else { 0 } + text = $Version + } +} + +function Test-PackageCompatibility { + param([string]$Id, [string]$Version, [string[]]$Frameworks, [string]$Workspace) + + $projectPath = Join-Path $Workspace 'compatibility.csproj' + $frameworkElement = if ($Frameworks.Count -eq 1) { + "$($Frameworks[0])" + } else { + "$($Frameworks -join ';')" + } + $xml = @" + + + $frameworkElement + $(Join-Path $Workspace 'packages') + + + + + +"@ + [System.IO.File]::WriteAllText($projectPath, $xml, $utf8NoBom) + $output = @(& dotnet restore $projectPath --nologo --verbosity quiet --force-evaluate 2>&1) + return [pscustomobject]@{ + compatible = $LASTEXITCODE -eq 0 + output = ($output -join [Environment]::NewLine) + } +} + +foreach ($framework in $TargetFramework) { + if ($framework -notmatch '^net(?:standard)?\d+(?:\.\d+)+$|^net\d{2,3}$') { + throw "Unsupported target framework syntax: '$framework'." + } +} + +$packageIds = if ($PackageId -and $PackageId.Count -gt 0) { + @($PackageId) +} else { + $codebeltPackage = if ($Role -eq 'Unit') { 'Codebelt.Extensions.Xunit' } else { 'Codebelt.Extensions.Xunit.App' } + @('Microsoft.NET.Test.Sdk', 'xunit.v3', 'xunit.v3.runner.console', 'xunit.runner.visualstudio', $codebeltPackage) +} + +$serviceIndex = Invoke-RestMethod -Uri 'https://api.nuget.org/v3/index.json' +$packageBaseAddress = $serviceIndex.resources | + Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | + Select-Object -First 1 -ExpandProperty '@id' +if ([string]::IsNullOrWhiteSpace($packageBaseAddress)) { throw 'NuGet service index did not expose PackageBaseAddress/3.0.0.' } + +$workspace = Join-Path ([System.IO.Path]::GetTempPath()) ('dotnet-test-package-resolution-' + [Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $workspace -Force | Out-Null + +try { + $resolved = [System.Collections.Generic.List[object]]::new() + foreach ($id in @($packageIds | Sort-Object -Unique)) { + $indexUrl = '{0}{1}/index.json' -f $packageBaseAddress, $id.ToLowerInvariant() + try { $index = Invoke-RestMethod -Uri $indexUrl } catch { throw "NuGet lookup failed for '$id' at '$indexUrl': $($_.Exception.Message)" } + $candidates = @($index.versions | + Where-Object { $_ -match '^\d+(?:\.\d+){1,3}$' } | + ForEach-Object { Get-VersionKey -Version $_ } | + Sort-Object major, minor, patch, revision -Descending | + Select-Object -First $MaximumCandidates) + if ($candidates.Count -eq 0) { throw "NuGet returned no stable versions for '$id'." } + + $selected = $null + $lastFailure = $null + foreach ($candidate in $candidates) { + $packageWorkspace = Join-Path $workspace ([System.IO.Path]::GetRandomFileName()) + New-Item -ItemType Directory -Path $packageWorkspace -Force | Out-Null + $compatibility = Test-PackageCompatibility -Id $id -Version $candidate.text -Frameworks $TargetFramework -Workspace $packageWorkspace + if ($compatibility.compatible) { + $selected = $candidate.text + break + } + $lastFailure = $compatibility.output + } + if ($null -eq $selected) { + throw "No stable '$id' version among the newest $($candidates.Count) candidates restored for '$($TargetFramework -join ';')'. Last restore output:`n$lastFailure" + } + $resolved.Add([pscustomobject]@{ packageId = $id; version = $selected; source = $indexUrl; compatibility = 'isolated restore passed' }) + } + + [ordered]@{ + role = $Role + targetFrameworks = @($TargetFramework) + packages = @($resolved | Sort-Object packageId) + } | ConvertTo-Json -Depth 5 +} finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force } +} + diff --git a/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 b/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 new file mode 100644 index 0000000..600d074 --- /dev/null +++ b/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 @@ -0,0 +1,90 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) +$workspace = Join-Path ([System.IO.Path]::GetTempPath()) ('dotnet-test-inspection-' + [Guid]::NewGuid().ToString('N')) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Path $Path -Parent + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + Write-File -Path (Join-Path $workspace 'Directory.Packages.props') -Content @' +true +'@ + Write-File -Path (Join-Path $workspace 'app/App.csproj') -Content @' +net10.0Exe +'@ + Write-File -Path (Join-Path $workspace 'app/Program.cs') -Content @' +using Codebelt.Bootstrapper.Web; public class Program : MinimalWebProgram { public static void Main(string[] args) { var builder = CreateHostBuilder(args); var app = builder.Build(); app.MapGet("/", () => "ok"); app.Run(); } } +'@ + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/App.FunctionalTests.csproj') -Content @' +net10.0true +'@ + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Microsoft.AspNetCore.Mvc.Testing; using Xunit.Abstractions; public class HealthTest : IClassFixture> { } +'@ + Write-File -Path (Join-Path $workspace 'src/Widget/Widget.csproj') -Content @' +net10.0 +'@ + Write-File -Path (Join-Path $workspace 'test/Widget.Tests/Widget.Tests.csproj') -Content @' +net10.0true +'@ + Write-File -Path (Join-Path $workspace 'worker/Worker.csproj') -Content @' +net10.0Exe +'@ + Write-File -Path (Join-Path $workspace 'worker/Program.cs') -Content @' +using Codebelt.Bootstrapper.Worker; public class Program : MinimalWorkerProgram { public static async Task Main(string[] args) { var builder = CreateHostBuilder(args); await builder.Build().RunAsync(); } } +'@ + Write-File -Path (Join-Path $workspace 'test/Worker.FunctionalTests/Worker.FunctionalTests.csproj') -Content @' +net10.0true +'@ + Write-File -Path (Join-Path $workspace 'console/Console.csproj') -Content @' +net10.0Exe +'@ + Write-File -Path (Join-Path $workspace 'console/Program.cs') -Content @' +using Codebelt.Bootstrapper.Console; public class Program : MinimalConsoleProgram { public static async Task Main(string[] args) { var builder = CreateHostBuilder(args); await builder.Build().RunAsync(); } public override Task RunAsync(IServiceProvider services, CancellationToken token) => Task.CompletedTask; } +'@ + Write-File -Path (Join-Path $workspace 'test/Console.FunctionalTests/Console.FunctionalTests.csproj') -Content @' +net10.0true +'@ + Write-File -Path (Join-Path $workspace 'legacy/Legacy.csproj') -Content @' +net10.0Exe +'@ + Write-File -Path (Join-Path $workspace 'legacy/Program.cs') -Content @' +System.Console.WriteLine("legacy"); +'@ + Write-File -Path (Join-Path $workspace 'test/Legacy.FunctionalTests/Legacy.FunctionalTests.csproj') -Content @' +net10.0true +'@ + + $scriptPath = Join-Path $PSScriptRoot 'inspect-dotnet-tests.ps1' + $json = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace + if ($LASTEXITCODE -ne 0) { throw "Inspection script exited with $LASTEXITCODE." } + $report = $json | ConvertFrom-Json + if ($report.projectCount -ne 5) { throw "Expected five projects, found $($report.projectCount)." } + $webProject = $report.projects | Where-Object project -eq 'test/App.FunctionalTests/App.FunctionalTests.csproj' + $unitProject = $report.projects | Where-Object project -eq 'test/Widget.Tests/Widget.Tests.csproj' + $workerProject = $report.projects | Where-Object project -eq 'test/Worker.FunctionalTests/Worker.FunctionalTests.csproj' + $consoleProject = $report.projects | Where-Object project -eq 'test/Console.FunctionalTests/Console.FunctionalTests.csproj' + $legacyProject = $report.projects | Where-Object project -eq 'test/Legacy.FunctionalTests/Legacy.FunctionalTests.csproj' + if ($webProject.role -ne 'ASP.NET Core functional test') { throw "Unexpected web role: $($webProject.role)" } + if ($unitProject.role -ne 'Ordinary unit test') { throw "Unexpected unit role: $($unitProject.role)" } + if ($workerProject.role -ne 'Console or worker functional test') { throw "Unexpected worker role: $($workerProject.role)" } + if ($consoleProject.role -ne 'Console or worker functional test') { throw "Unexpected console role: $($consoleProject.role)" } + if ($legacyProject.role -ne 'Console or worker functional test' -or $legacyProject.blockers.Count -lt 1) { throw 'Expected legacy executable to report the Generic Host blocker.' } + if ($webProject.xunitGeneration -ne 'v2') { throw "Unexpected xUnit generation: $($webProject.xunitGeneration)" } + if ($webProject.webApplicationFactoryUsages.Count -lt 1) { throw 'Expected WebApplicationFactory usage.' } + if (@($webProject.packageOwnership | Where-Object { $_.id -eq 'xunit' -and $_.ownership -eq 'central' }).Count -ne 1) { throw 'Expected central xunit package ownership.' } + if (-not $webProject.referencedApplications[0].genericHost -or -not $webProject.referencedApplications[0].webHost) { throw 'Expected a detectable web Generic Host.' } + if ($webProject.referencedApplications[0].hostPattern -ne 'MinimalWebProgram') { throw 'Expected MinimalWebProgram detection.' } + if ($workerProject.referencedApplications[0].hostPattern -ne 'MinimalWorkerProgram') { throw 'Expected MinimalWorkerProgram detection.' } + if ($consoleProject.referencedApplications[0].hostPattern -ne 'MinimalConsoleProgram') { throw 'Expected MinimalConsoleProgram detection.' } + + Write-Host 'inspect-dotnet-tests.ps1 regression: PASS' +} finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force } +} diff --git a/skills/dotnet-test/scripts/validate-skill.ps1 b/skills/dotnet-test/scripts/validate-skill.ps1 new file mode 100644 index 0000000..c8aec58 --- /dev/null +++ b/skills/dotnet-test/scripts/validate-skill.ps1 @@ -0,0 +1,31 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$skillRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + +$required = @( + 'SKILL.md', 'FORMS.md', 'evals/evals.json', + 'scripts/inspect-dotnet-tests.ps1', 'scripts/resolve-test-package-versions.ps1', 'scripts/test-inspect-dotnet-tests.ps1', + 'references/unit-tests.md', 'references/web-functional-tests.md', 'references/application-functional-tests.md', + 'references/bootstrapper-hosts.md', 'references/xunit-v3-modernization.md', 'references/migration-invariants.md', + 'assets/unit/BehaviorTest.cs', 'assets/web/FocusedWebApplicationTest.cs', 'assets/web/SharedWebApplicationTest.cs', + 'assets/application/FocusedApplicationTest.cs', 'assets/application/SharedApplicationTest.cs', + 'assets/bootstrapper/console/Program.cs', 'assets/bootstrapper/console/Startup.cs', + 'assets/bootstrapper/worker/Program.cs', 'assets/bootstrapper/worker/Startup.cs', 'assets/bootstrapper/worker/Worker.cs', + 'assets/bootstrapper/console-minimal/Program.cs', 'assets/bootstrapper/worker-minimal/Program.cs', + 'assets/bootstrapper/web-minimal/Program.cs' +) + +foreach ($relative in $required) { + if (-not (Test-Path -LiteralPath (Join-Path $skillRoot $relative) -PathType Leaf)) { throw "Missing required dotnet-test file: $relative" } +} + +$skill = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'SKILL.md')) +foreach ($needle in @('WebApplicationTestFactory', 'ApplicationTestFactory', 'BlockingManagedWebApplicationFixture', 'BlockingManagedApplicationFixture', 'zero remaining `WebApplicationFactory`')) { + if (-not $skill.Contains($needle, [System.StringComparison]::Ordinal)) { throw "SKILL.md is missing required contract: $needle" } +} +if (-not $skill.Contains('An MTP executable run may supplement that gate but never replaces it', [System.StringComparison]::Ordinal)) { throw 'SKILL.md must reject MTP executable substitution for requested dotnet test validation.' } + +& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'test-inspect-dotnet-tests.ps1') +if ($LASTEXITCODE -ne 0) { throw "Inspection regression failed with exit code $LASTEXITCODE." } + +Write-Host 'dotnet-test skill validation: PASS' From 4427bf6ffa4a3afde199abbeb7a3e80d4a824d76 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 2 Aug 2026 16:47:49 +0200 Subject: [PATCH 02/16] =?UTF-8?q?=F0=9F=94=A7=20add=20dotnet-test=20valida?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive validation checks for the new dotnet-test skill. Verifies SKILL.md content, FORMS.md field definitions, role-specific reference documents (web, application, bootstrapper, modernization, migration), inspection and version-resolution scripts, eval scenarios with five paired test cases covering web, unit, v2 modernization, and worker patterns, and fixture directory structure. Prevents bin/obj directories in eval files and runs skill-level validation. --- scripts/validate-skill-templates.ps1 | 77 ++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index e74d1a0..9500dbd 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -1087,6 +1087,83 @@ Add-ValidationResult -Results $results -Name 'Benchmark runner wildcard is prese Assert-Match -Name 'benchmark-program.cs' -Content $program -Pattern 'namespace\s+\{BENCHMARK_RUNNER_NAMESPACE\};' } +Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific Codebelt xUnit migration and bootstrap contracts' -Action { + $skill = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/SKILL.md' -GitRef $Ref + $forms = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/FORMS.md' -GitRef $Ref + $web = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/references/web-functional-tests.md' -GitRef $Ref + $application = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/references/application-functional-tests.md' -GitRef $Ref + $bootstrapper = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/references/bootstrapper-hosts.md' -GitRef $Ref + $modernization = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/references/xunit-v3-modernization.md' -GitRef $Ref + $migration = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/references/migration-invariants.md' -GitRef $Ref + $inspect = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/scripts/inspect-dotnet-tests.ps1' -GitRef $Ref + $resolve = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/scripts/resolve-test-package-versions.ps1' -GitRef $Ref + $evals = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/evals/evals.json' -GitRef $Ref + $fixtureFiles = Get-RepoFileList -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/evals/files' -GitRef $Ref + + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Ordinary unit test' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ASP.NET Core functional test' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Console or worker functional test' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'WebApplicationTestFactory.Create' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'WebApplicationTest>' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ApplicationTestFactory.Create' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ApplicationTest>' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Never add a process-launching fallback' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'zero remaining `WebApplicationFactory`' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Do not invent an endpoint, service, configuration key, or expected result.' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'An MTP executable run may supplement that gate but never replaces it' + Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### project_selection' + Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### operation_mode' + Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### test_role' + Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle 'Field: ' + Assert-Contains -Name 'dotnet-test/web-functional-tests.md' -Content $web -Needle 'BlockingManagedWebApplicationFixture' + Assert-Contains -Name 'dotnet-test/application-functional-tests.md' -Content $application -Needle 'Do not introduce `Process.Start`' + foreach ($program in @('MinimalConsoleProgram', 'MinimalWorkerProgram', 'MinimalWebProgram')) { + Assert-Contains -Name 'dotnet-test/bootstrapper-hosts.md' -Content $bootstrapper -Needle $program + Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle $program + } + Assert-Contains -Name 'dotnet-test/xunit-v3-modernization.md' -Content $modernization -Needle 'true' + Assert-Contains -Name 'dotnet-test/xunit-v3-modernization.md' -Content $modernization -Needle 'A zero-discovery test command is not a successful test run' + Assert-Contains -Name 'dotnet-test/migration-invariants.md' -Content $migration -Needle 'lazy until `CreateClient`, `Server`, or `Services`' + Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle '-getProperty:TargetFramework,TargetFrameworks,IsTestProject,OutputType,ManagePackageVersionsCentrally,UseMicrosoftTestingPlatformRunner,RootNamespace' + Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'webApplicationFactoryUsages' + Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'packageOwnership' + Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'https://api.nuget.org/v3/index.json' + Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'isolated restore passed' + + $evalObject = $evals | ConvertFrom-Json + if (@($evalObject.evals).Count -ne 5) { + throw "dotnet-test must define exactly five requested paired eval scenarios; found $(@($evalObject.evals).Count)" + } + foreach ($needle in @('attached Acme.Calculator fixture', 'xUnit v2 project', 'web-cdn-origin-style', 'IClassFixture>', 'ApplicationTestFactory pattern')) { + Assert-Contains -Name 'dotnet-test/evals/evals.json' -Content $evals -Needle $needle + } + if (@($fixtureFiles | Where-Object { $_ -match '(^|[\\/])(bin|obj)([\\/]|$)' }).Count -gt 0) { + throw 'dotnet-test eval fixtures must not include bin/ or obj/ paths' + } + + foreach ($asset in @( + 'skills/dotnet-test/assets/unit/BehaviorTest.cs', + 'skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs', + 'skills/dotnet-test/assets/web/SharedWebApplicationTest.cs', + 'skills/dotnet-test/assets/application/FocusedApplicationTest.cs', + 'skills/dotnet-test/assets/application/SharedApplicationTest.cs', + 'skills/dotnet-test/assets/bootstrapper/console/Program.cs', + 'skills/dotnet-test/assets/bootstrapper/worker/Program.cs', + 'skills/dotnet-test/assets/bootstrapper/console-minimal/Program.cs', + 'skills/dotnet-test/assets/bootstrapper/worker-minimal/Program.cs', + 'skills/dotnet-test/assets/bootstrapper/web-minimal/Program.cs' + )) { + [void](Get-FileText -RepoRoot $repoRoot -RelativePath $asset -GitRef $Ref) + } + + if ([string]::IsNullOrWhiteSpace($Ref)) { + & pwsh -NoProfile -File (Join-Path $repoRoot 'skills/dotnet-test/scripts/validate-skill.ps1') + if ($LASTEXITCODE -ne 0) { + throw "dotnet-test skill validation failed with exit code $LASTEXITCODE." + } + } +} + Add-ValidationResult -Results $results -Name 'dotnet-benchmark enforces valid, proportionate experiments and preserves honest comparison semantics' -Action { $skill = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-benchmark/SKILL.md' -GitRef $Ref $forms = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-benchmark/FORMS.md' -GitRef $Ref From cd237534c99c9a18fd7c58bbdafba18702b64c58 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 2 Aug 2026 16:48:12 +0200 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=92=AC=20document=20dotnet-test=20r?= =?UTF-8?q?elease?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add v0.9.0 release notes documenting the new dotnet-test skill in CHANGELOG.md following Keep a Changelog format. Update README skill inventory to include dotnet-test entry with installation instructions. Add motivational section explaining the skills purpose: lifecycle-sensitive test migration that preserves WebApplicationFactory configuration, lazy startup, and Generic Host seams while routing to role-specific patterns (ordinary unit, ASP.NET Core functional, console/worker functional). --- CHANGELOG.md | 33 ++++++++++++++++++++++++++++++++- README.md | 21 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 019a7a4..7731580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.9.0 - 2026-08-02 + +### Added + +- `dotnet-test` skill for classifying, bootstrapping, and refactoring xUnit projects across ordinary unit, ASP.NET Core functional, and console/worker functional roles, +- Deterministic .NET test inspection and NuGet-backed compatible package-resolution scripts, structured forms, role references, adaptable Codebelt xUnit/Bootstrapper assets, and five paired evaluation fixtures, +- Repository validation for the `dotnet-test` lifecycle, modernization, Generic Host, fixture, script, asset, and evaluation contracts. +- Explicit recognition and adaptable assets for Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` host families. + +### Changed + +- Updated the root skill catalogue, install bundle, installation examples, and rationale documentation for `dotnet-test`. + +## [0.8.1] - 2026-08-01 + +This is a minor release focused on tightening `agent-smith` skill guidance for clearer communication and parallelism, adding deterministic validators for skill-template compliance, and refining `git-visual-commits` with improved single-category quality gates and clearer auto-approval triggering semantics. + +### Added + +- `ValidateSkillTemplates` coverage in skill-template validator recognizing and validating `agent-smith` and `git-visual-commits` structural requirements, +- Enhanced validation tooling in `scripts/validate-skill-templates.ps1` for skill contract enforcement. + +### Changed + +- Refactored `agent-smith` SKILL.md guidance for conciseness, parallelism, and clearer presentation of engineering-discipline workflows, +- Restructured `agent-smith` skill-authoring reference with consolidated Anthropic skill-authoring best practices, three-level progressive disclosure guidance, and improved field definitions, +- Refined `git-visual-commits` single-category quality gate documentation with explicit audit workflow and per-file rationale checks, +- Clarified `git-visual-commits` auto-approval triggering semantics distinguishing between auto-approval modifiers (`yolo`/`auto`) within explicit commit requests versus standalone non-commit invocations, +- Updated README with refined `agent-smith` description and enhanced `git-visual-commits` quality-gate documentation. + ## [0.8.0] - 2026-07-18 This is a minor release introducing the `agent-smith` skill for rigorous software-craftsmanship standards across design, architecture, implementation, testing, performance, security, DevSecOps, and CI/CD, alongside the `dotnet-benchmark` skill for evidence-driven performance testing. The release resolves a critical git-keep-a-changelog bug that could silently include already-released commits when determining scope boundaries, replaces implicit caret notation with deterministic branch-derived scope validation, and introduces deterministic commit-subject validation infrastructure to `git-visual-commits` with a bundled PowerShell validator and full-skill-read gating. PowerShell execution is standardized to pwsh 7+, and skill validation tooling is strengthened across the repository. @@ -493,7 +523,8 @@ This is a minor release that introduces two complementary git workflow skills, e - Improved scaffold fidelity with hidden `.bot` asset preservation, explicit UTF-8 and BOM handling, and checks aimed at preventing mojibake or incomplete generated output. -[Unreleased]: https://github.com/codebeltnet/agentic/compare/v0.8.0...HEAD +[Unreleased]: https://github.com/codebeltnet/agentic/compare/v0.8.1...HEAD +[0.8.1]: https://github.com/codebeltnet/agentic/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/codebeltnet/agentic/compare/v0.7.5...v0.8.0 [0.7.5]: https://github.com/codebeltnet/agentic/compare/v0.7.4...v0.7.5 [0.7.4]: https://github.com/codebeltnet/agentic/compare/v0.7.3...v0.7.4 diff --git a/README.md b/README.md index c38a7ea..5a6bf65 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ npx skills add https://github.com/codebeltnet/agentic --skill dotnet-new-lib-sln npx skills add https://github.com/codebeltnet/agentic --skill git-remote-release npx skills add https://github.com/codebeltnet/agentic --skill dotnet-change-impact npx skills add https://github.com/codebeltnet/agentic --skill dotnet-docfx-digest +npx skills add https://github.com/codebeltnet/agentic --skill dotnet-test npx skills add https://github.com/codebeltnet/agentic --skill dotnet-benchmark npx skills add https://github.com/codebeltnet/agentic --skill agent-smith # npx skills add https://github.com/codebeltnet/agentic --skill another-skill @@ -118,6 +119,7 @@ npx skills add https://github.com/codebeltnet/agentic --skill agent-smith | [git-remote-release](skills/git-remote-release/SKILL.md) | Generate GitHub release notes by summarizing all commits and pull requests between two Git tags or branches in a remote GitHub repository. Accepts a compare URL or separate owner/repo, previous ref, and current ref values; falls back to comparing the current branch against the upstream default branch when no input is provided. Produces a human-friendly `## What's Changed` summary with optional GitHub alert blocks, a `Sources:` section preserving PR and commit references, and a full changelog compare link. | | [dotnet-change-impact](skills/dotnet-change-impact/SKILL.md) | Classify .NET library or NuGet package changes and recommend the correct release bump — `Major`, `Minor`, or `Patch` — for both Semantic Versioning (`MAJOR.MINOR.PATCH`) and .NET assembly/file versioning (`Major.Minor.Build.Revision`), grounded in Microsoft's official .NET compatibility rules. Uses the current Git branch by default when no explicit change details or compare range are provided, resolving it against the upstream/default base branch with local read-only git state. Always returns structured behavioral/binary/source/design-time/backwards compatibility reasoning with the recommendation, even when the bump is clear. | | [dotnet-docfx-digest](skills/dotnet-docfx-digest/SKILL.md) | Create and maintain developer-friendly DocFX documentation for .NET public APIs, including repo-wide no-input audits that inspect source, tests, DocFX config, DocFX `build.content` and `build.overwrite` Markdown inputs, namespace pages, and availability includes before asking for clarification, while treating bare direct skill invocations as autonomous repo-wide runs rather than human-driven checkpoint sessions. Enforces the workflow with two bundled .NET 10 file-based scripts resolved from the loaded skill directory, falling back to the repo-managed source path only when present: `scripts/agents.cs` writes an idempotent, marker-bounded DocFX maintenance block into the repository `AGENTS.md`; `scripts/docfx.cs` is **fast and build-free by default** — it validates Markdown, prose, DocFX overwrite layout, namespace overview pages, `Extension Members` tables, decorated receiver signatures such as `IDecorator`, generic method displays such as `As`, purpose-first summaries, and required per-type/extension examples without invoking `dotnet`, `msbuild`, `docfx`, or `gh`, discovering the public API from existing DocFX YAML metadata or a conservative source scan and ending every run with a `[processes] dotnet=0 msbuild=0 docfx=0 gh=0` summary plus per-phase timings. Compilation and network access are strictly opt-in: `--validate-samples` compiles each C# sample in an isolated project while batching all sample projects into one temporary `.slnx` graph build with bounded MSBuild parallelism and scoped references, `--build-api-model` (alias `--strict-api-discovery`) does reflection-backed discovery from compiled metadata via `MetadataLoadContext` through a single scoped `.slnx` graph build, `--verify-docfx-build` runs the DocFX CLI in a temp copy, and `--search-examples` runs `gh` code search. Final verification adapts to available processors and memory, overlaps isolated DocFX work on high-capacity machines, uses a 30-minute child timeout, and emits 10-second `stderr` heartbeats with active phase, workload, runner count, PID, elapsed time, last-output age, and current child output while preserving machine-readable JSON on `stdout`. Honors a single DocFX metadata `TargetFramework` when `--framework` is omitted, collapses C# 14 extension-block compiler containers such as `$...` back to the authored outer static class in both fast DocFX-YAML discovery and build-backed reflection discovery, validates namespace fly-ins that explain the problem solved/when to use/where to start plus example fly-ins before every C# fence, the Codebelt namespace-and-type-folder overwrite layout (`.docfx/api/namespaces/**/*.md` and `.docfx/api/types/**/*.md` under `build.overwrite` only), keeps `--changed-only` validation scoped to affected docs and APIs while still including brand-new untracked overwrite Markdown, uses the root Codebelt `.snk` when present and falls back to `-p:SkipSignAssembly=true` for keyless strong-name build verification, drains child stdout and stderr concurrently to avoid verbose-build deadlocks, writes deterministic `--assessment-queue` Markdown work queues for noisy audits, preserves working URL references unless a verified HTTP 404 justifies removal, treats unexpected new repo-root or DocFX-workspace files that are not known `dotnet-docfx-digest` deliverables as blocking cleanup diagnostics, keeps assessment/manifests/captured output/helper scripts in temp or session storage instead of the target repository, requires a namespace-first pass across the active queue before net-new type/example authoring during full audits, keeps deeper `EXTENSION_METHOD_MISSING` and `EXTENSION_METHOD_SIGNATURE_MISSING` follow-on diagnostics in that same namespace-layer table-repair phase when they appear after `EXTENSION_SECTION_MISSING` drops, preserves existing BOM and line-ending state while flagging actual mojibake instead of creating encoding-only diffs, and leaves generated DocFX YAML metadata untouched unless `--clean-generated-metadata` is explicitly requested (which runs only after the API model is built, never deleting metadata the run relied on). Documents public API only, uses bundled reference docs for overwrite rules, workflow details, and script behavior, keeps authored API overwrite Markdown under `.docfx/api/namespaces/` and `.docfx/api/types/`, moves legacy authored `.docfx/api/*.md` overwrite files there instead of widening the glob to `api/**/*.md`, teaches namespace and API prose to orient newcomers around purpose instead of inventorying contents, prefers inline or small sibling-batch prose repairs over slow per-page worker fan-out, makes examples start from package-ID usage evidence before type/member-only searches and requires each example to introduce the consumer task before the code, allows multi-type Microsoft Learn-style scenario samples when they better explain the consumer workflow, keeps extension-method examples on readable declaring-class type pages under `.docfx/api/types/` instead of synthetic method-UID filenames or namespace pages that mix extra `uid:` / `example:` blocks into the overview, flags weak skip-compile reasons, requires deterministic `.docfx/skip-compile-allowlist.json` entries for any pre-existing approved skip waivers, treats newly introduced or unallowlisted skip markers as fail-level diagnostics that do not suppress compilation, establishes reflection-backed packets with `--build-api-model --project-manifest` before full-run authoring, forces mid-audit continuations to name that manifest or the sequential assessment/namespace-first fallback explicitly, requires those continuations to restate the fast `docfx.cs --json` rerun cadence, the exact final `docfx.cs --build-api-model --validate-samples --verify-docfx-build --json` gate, and the clean JSON completion contract instead of generic “verify later” prose, treats batch size only as rerun cadence rather than permission to stop, runs a completion repair loop that treats every diagnostic as active work regardless of age or volume, treats newly surfaced follow-on diagnostics as the next repair queue instead of a stop point, reruns packet discovery with `--build-api-model --project-manifest` when fast source-scan packets are unnamed or zero-project, falls back to sequential namespace-first or assessment work queue order when packet discovery is still unusable, treats `EXAMPLE_MISSING`, `EXAMPLE_LEAD_MISSING`, `EXAMPLE_ADVANCED_LEAD_MISSING`, `FAMILY_ANCHOR_EXAMPLE_MISSING`, `SAMPLE_STRUCTURE_INVALID`, `FAIL_NEW_SKIP_MARKER_INTRODUCED`, `SAMPLE_SKIP_NOT_ALLOWLISTED`, and `INTERIM_ARTIFACT_IN_WORKTREE` queues as core work rather than checkpoints or quality backlog, drives large example and lead queues through a concrete fast-path micro-loop (next item or next 3-5 items → rerun → continue), suppresses progress-table/checkpoint output until the completion contract is clean or a real external blocker is reported, treats premature completion-shaped handoffs as execution-protocol failures while the queue is still dirty, reserves the final `--build-api-model --validate-samples --verify-docfx-build` verification for the real end of the queue, exposes `summary.fullVerificationRan`, `summary.canClaimCompletion`, `summary.remainingWorkItems`, `summary.remainingDiagnosticsByCode`, `summary.newlyIntroducedSkipMarkers`, and `summary.interimArtifacts` as machine-readable final gates, reruns the fast `docfx.cs --json` after edits until the queue is empty, then runs the build-backed verification before completion, preserves manual edits and authored Markdown during cleanup, skips recursive generated-output cleanup when a target directory contains documentation or source files, and returns deterministic exit codes plus `--json` reports (including process counts, phase timings, warning counts, and skip-marker accounting) so CI can gate on real failures instead of AI claims. | +| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated restores. Focused web tests keep `Test` ownership and use `WebApplicationTestFactory`; shared web fixtures use `WebApplicationTest` with `BlockingManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory`; and shared non-web fixtures use `ApplicationTest` with `BlockingManagedApplicationFixture`. Migrations preserve host configuration, lazy start, clients, services, configuration, disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | | [dotnet-benchmark](skills/dotnet-benchmark/SKILL.md) | Discovers, prioritizes, and authors trustworthy BenchmarkDotNet experiments for a .NET type following codebelt conventions and using the `Codebelt.Extensions.BenchmarkDotNet.Console` runner. It inspects implementation code, call sites, tests, existing benchmarks, and available profiles instead of benchmarking every public member; ranks likely high-impact operations; selects representative typical, boundary, scaling, and adverse cases; and rejects external-I/O or service-level questions that need profiling, macrobenchmarks, or load tests. It creates fair current-versus-candidate comparisons only when observable work is equivalent, uses baseline-free single-operation characterization when no honest comparator exists, prevents unrelated construction/formatting/equality/hash ratios, requires exact per-case correctness oracles plus a semantic preflight for truthful workload labels, hard-gates interpretation on a complete valid BenchmarkDotNet summary, preserves workload invariants such as selectivity and hit/miss ratios as sizes scale, distinguishes deferred pipeline creation from terminal/materialization work, and performs Release build, discovery listing, and dry execution before any explicit full run. Explicit `yolo` mode auto-accepts routine repo-derived defaults and the proposed plan, then proceeds through build/list/dry validation without confirmation churn; only a separate explicit human instruction can start a full performance run. Its runner preflight recognizes the standard Slim/runtime setup and explains when `SkipBenchmarksWithReports = true` plus a matching `reports/tuning/` artifact deliberately filters a benchmark, preventing needless class renames, disassembly, or tool thrash; after the first valid full result it stops unless deeper diagnostics could change a real engineering decision. Harness setup remains adaptive: it detects `.slnx`/`.sln`, CPM, existing `tuning/` projects, and a reusable `tooling/` runner, onboards only missing pieces, resolves package versions dynamically, and keeps the benchmark class in the SUT namespace. | | [agent-smith](skills/agent-smith/SKILL.md) | Apply a rigorous, consistent, evidence-driven software-craftsmanship standard across a whole engineering task. Invoke explicitly as `/agent-smith ` or let it auto-trigger for design, architecture, implementation, refactoring, code review, public API review, compatibility and Semantic Versioning analysis, testing, benchmarking, performance, skill authoring, documentation, security and DevSecOps, CI/CD, delivery, repository governance, and engineering assessment. Skill-authoring mode grounds instructions in real execution, requires an explicit bounded-concurrency assessment so independent data retrieval and eval work do not remain sequential by habit, favors reusable C#/.NET scripts and validators against the dynamically resolved latest supported LTS when local constraints do not decide, and follows the Agent Skills guidance for progressive disclosure, description optimization, candidate-versus-baseline evaluation, aggregation, and human review. Its optional .NET EditorConfig conformance mode handles targeted IDE/CA diagnostic remediation and full informational-or-higher `dotnet format` conformance without treating a clean build as proof of policy compliance: user-defined diagnostic IDs remain task-supplied data; target, path, and severity scope remains authoritative; informational workflows explicitly preserve `--severity info` because the formatter defaults to `warn`; targeted IDE and analyzer checks use category-specific formatter subcommands; every formatter invocation is read-only via `--verify-no-changes`; `--no-restore` is never treated as a conformance fallback; fixes are deliberate source edits; repeated multi-target findings are de-duplicated by physical file, diagnostic, and span; and the bundled `repair-roslyn-multiproject-artifacts.ps1` detects conflict artifacts independently of diagnostic ID, preflights directory repairs without partial writes, repairs only proven structural patterns, and refuses unrecognized shapes. Completion requires the same scoped formatter gate plus an artifact scan before affected builds and relevant tests. Technology-neutral work remains unaffected. Performs the requested work (not just a review), loads only relevant `references/`, respects repository conventions, scales process depth without lowering the standard, and reports evidence and risk honestly in concise feedback that may sacrifice grammar but never required evidence. Governing principle: consistency is key. | @@ -220,6 +222,11 @@ npx skills add https://github.com/codebeltnet/agentic --skill dotnet-docfx-diges ```bash npx skills add https://github.com/codebeltnet/agentic --skill dotnet-benchmark ``` +`dotnet-test` + +```bash +npx skills add https://github.com/codebeltnet/agentic --skill dotnet-test +``` `agent-smith` ```bash @@ -608,6 +615,20 @@ API documentation rots the moment code changes. A new public type ships without - **Cleanup keeps authored docs and is opt-in** — generated-metadata cleanup runs only when `--clean-generated-metadata` is explicitly passed, and even then only after the API model is built so it never deletes YAML the run relied on; `.docfx/**/*.md` overwrite files, namespace pages, includes, and config are documentation outputs, not disposable build artifacts, and cleanup is limited to known metadata files and safe site-output directories that contain no authored documentation or source files, - **CI-friendly** — deterministic exit codes plus `--json` reports let pipelines fail on actual documentation drift instead of trusting an agent's claim that it checked. +### Why dotnet-test? + +Test-project refactoring is deceptively lifecycle-sensitive. A `WebApplicationFactory` wrapper may own temporary directories, defer host startup until the first client, replace services in a specific order, or isolate settings per test. Console and worker tests have a different boundary: they need a resolvable in-process Generic Host, not a child process hidden behind a test helper. + +**dotnet-test** begins with machine-readable inspection, then chooses the Codebelt pattern that matches the selected project's role and ownership model. It preserves package ownership and frameworks, migrates xUnit v2 to v3/Microsoft Testing Platform when needed, and makes zero remaining selected `WebApplicationFactory` usages plus restore/build/test explicit gates. + +- **Three explicit roles** — ordinary unit, ASP.NET Core functional, and console/worker functional tests route to separate references and assets, +- **Lifecycle-preserving web migration** — focused factory and shared blocking-fixture patterns retain configuration, lazy start, client/service access, disposal, and isolation, +- **Generic Host boundary** — non-web tests use `ApplicationTestFactory` or `ApplicationTest`; missing host seams are reported precisely unless production adaptation is authorized, +- **Bootstrapper host fidelity** — Startup-based hosts and `MinimalConsoleProgram`, `MinimalWorkerProgram`, or `MinimalWebProgram` hosts remain in their established family instead of being rewritten for test convenience, +- **Dynamic compatibility** — stable package versions come from NuGet and must pass an isolated restore for the selected target frameworks, +- **Source-grounded bootstrap** — new projects receive at least one behavior test derived from real source instead of a placeholder, +- **Deterministic evidence** — inspection JSON reports roles, frameworks, xUnit generation, package owners, inheritance, migrations, recommendations, and blockers before mutation. + ### Why dotnet-benchmark? Setting up a benchmark "properly" is only half the problem. A benchmark can compile and still answer the wrong question: public members get measured because they are visible, unrelated operations share a meaningless baseline, random inputs miss real branches, setup leaks into the timed path, or a disk/service bottleneck is disguised as a microbenchmark. The result looks scientific but gives an engineer little trustworthy optimization evidence. From 74c73c1f66411e39caf904254acec9a5c66daf69 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 2 Aug 2026 17:43:15 +0200 Subject: [PATCH 04/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refine=20dotnet-test?= =?UTF-8?q?=20skill=20and=20eval=20scenarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify WebApplicationTestFactory bootstrap pattern in SKILL.md and references to ensure Program composition root is preserved without pipeline reconstruction in test code. Refine eval scenario 3 prompts and postconditions to emphasize focused inspector bootstrap contract and eliminate legacy WebApplicationFactory. Enhance inspect-dotnet-tests.ps1 with additional property discovery and validation checks. Add test-inspect-dotnet-tests.ps1 for script verification. Update migration-invariants.md and web-functional-tests.md reference docs with clearer lifecycle semantics. Improve validate-skill.ps1 postcondition checking. Update README inventory. --- README.md | 5 +- skills/dotnet-test/SKILL.md | 9 +++- skills/dotnet-test/evals/evals.json | 10 ++-- .../references/migration-invariants.md | 6 ++- .../references/web-functional-tests.md | 15 ++++++ .../scripts/inspect-dotnet-tests.ps1 | 53 +++++++++++++++++-- .../scripts/test-inspect-dotnet-tests.ps1 | 26 +++++++++ skills/dotnet-test/scripts/validate-skill.ps1 | 7 ++- 8 files changed, 116 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5a6bf65..6d64fa9 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ npx skills add https://github.com/codebeltnet/agentic --skill agent-smith | [git-remote-release](skills/git-remote-release/SKILL.md) | Generate GitHub release notes by summarizing all commits and pull requests between two Git tags or branches in a remote GitHub repository. Accepts a compare URL or separate owner/repo, previous ref, and current ref values; falls back to comparing the current branch against the upstream default branch when no input is provided. Produces a human-friendly `## What's Changed` summary with optional GitHub alert blocks, a `Sources:` section preserving PR and commit references, and a full changelog compare link. | | [dotnet-change-impact](skills/dotnet-change-impact/SKILL.md) | Classify .NET library or NuGet package changes and recommend the correct release bump — `Major`, `Minor`, or `Patch` — for both Semantic Versioning (`MAJOR.MINOR.PATCH`) and .NET assembly/file versioning (`Major.Minor.Build.Revision`), grounded in Microsoft's official .NET compatibility rules. Uses the current Git branch by default when no explicit change details or compare range are provided, resolving it against the upstream/default base branch with local read-only git state. Always returns structured behavioral/binary/source/design-time/backwards compatibility reasoning with the recommendation, even when the bump is clear. | | [dotnet-docfx-digest](skills/dotnet-docfx-digest/SKILL.md) | Create and maintain developer-friendly DocFX documentation for .NET public APIs, including repo-wide no-input audits that inspect source, tests, DocFX config, DocFX `build.content` and `build.overwrite` Markdown inputs, namespace pages, and availability includes before asking for clarification, while treating bare direct skill invocations as autonomous repo-wide runs rather than human-driven checkpoint sessions. Enforces the workflow with two bundled .NET 10 file-based scripts resolved from the loaded skill directory, falling back to the repo-managed source path only when present: `scripts/agents.cs` writes an idempotent, marker-bounded DocFX maintenance block into the repository `AGENTS.md`; `scripts/docfx.cs` is **fast and build-free by default** — it validates Markdown, prose, DocFX overwrite layout, namespace overview pages, `Extension Members` tables, decorated receiver signatures such as `IDecorator`, generic method displays such as `As`, purpose-first summaries, and required per-type/extension examples without invoking `dotnet`, `msbuild`, `docfx`, or `gh`, discovering the public API from existing DocFX YAML metadata or a conservative source scan and ending every run with a `[processes] dotnet=0 msbuild=0 docfx=0 gh=0` summary plus per-phase timings. Compilation and network access are strictly opt-in: `--validate-samples` compiles each C# sample in an isolated project while batching all sample projects into one temporary `.slnx` graph build with bounded MSBuild parallelism and scoped references, `--build-api-model` (alias `--strict-api-discovery`) does reflection-backed discovery from compiled metadata via `MetadataLoadContext` through a single scoped `.slnx` graph build, `--verify-docfx-build` runs the DocFX CLI in a temp copy, and `--search-examples` runs `gh` code search. Final verification adapts to available processors and memory, overlaps isolated DocFX work on high-capacity machines, uses a 30-minute child timeout, and emits 10-second `stderr` heartbeats with active phase, workload, runner count, PID, elapsed time, last-output age, and current child output while preserving machine-readable JSON on `stdout`. Honors a single DocFX metadata `TargetFramework` when `--framework` is omitted, collapses C# 14 extension-block compiler containers such as `$...` back to the authored outer static class in both fast DocFX-YAML discovery and build-backed reflection discovery, validates namespace fly-ins that explain the problem solved/when to use/where to start plus example fly-ins before every C# fence, the Codebelt namespace-and-type-folder overwrite layout (`.docfx/api/namespaces/**/*.md` and `.docfx/api/types/**/*.md` under `build.overwrite` only), keeps `--changed-only` validation scoped to affected docs and APIs while still including brand-new untracked overwrite Markdown, uses the root Codebelt `.snk` when present and falls back to `-p:SkipSignAssembly=true` for keyless strong-name build verification, drains child stdout and stderr concurrently to avoid verbose-build deadlocks, writes deterministic `--assessment-queue` Markdown work queues for noisy audits, preserves working URL references unless a verified HTTP 404 justifies removal, treats unexpected new repo-root or DocFX-workspace files that are not known `dotnet-docfx-digest` deliverables as blocking cleanup diagnostics, keeps assessment/manifests/captured output/helper scripts in temp or session storage instead of the target repository, requires a namespace-first pass across the active queue before net-new type/example authoring during full audits, keeps deeper `EXTENSION_METHOD_MISSING` and `EXTENSION_METHOD_SIGNATURE_MISSING` follow-on diagnostics in that same namespace-layer table-repair phase when they appear after `EXTENSION_SECTION_MISSING` drops, preserves existing BOM and line-ending state while flagging actual mojibake instead of creating encoding-only diffs, and leaves generated DocFX YAML metadata untouched unless `--clean-generated-metadata` is explicitly requested (which runs only after the API model is built, never deleting metadata the run relied on). Documents public API only, uses bundled reference docs for overwrite rules, workflow details, and script behavior, keeps authored API overwrite Markdown under `.docfx/api/namespaces/` and `.docfx/api/types/`, moves legacy authored `.docfx/api/*.md` overwrite files there instead of widening the glob to `api/**/*.md`, teaches namespace and API prose to orient newcomers around purpose instead of inventorying contents, prefers inline or small sibling-batch prose repairs over slow per-page worker fan-out, makes examples start from package-ID usage evidence before type/member-only searches and requires each example to introduce the consumer task before the code, allows multi-type Microsoft Learn-style scenario samples when they better explain the consumer workflow, keeps extension-method examples on readable declaring-class type pages under `.docfx/api/types/` instead of synthetic method-UID filenames or namespace pages that mix extra `uid:` / `example:` blocks into the overview, flags weak skip-compile reasons, requires deterministic `.docfx/skip-compile-allowlist.json` entries for any pre-existing approved skip waivers, treats newly introduced or unallowlisted skip markers as fail-level diagnostics that do not suppress compilation, establishes reflection-backed packets with `--build-api-model --project-manifest` before full-run authoring, forces mid-audit continuations to name that manifest or the sequential assessment/namespace-first fallback explicitly, requires those continuations to restate the fast `docfx.cs --json` rerun cadence, the exact final `docfx.cs --build-api-model --validate-samples --verify-docfx-build --json` gate, and the clean JSON completion contract instead of generic “verify later” prose, treats batch size only as rerun cadence rather than permission to stop, runs a completion repair loop that treats every diagnostic as active work regardless of age or volume, treats newly surfaced follow-on diagnostics as the next repair queue instead of a stop point, reruns packet discovery with `--build-api-model --project-manifest` when fast source-scan packets are unnamed or zero-project, falls back to sequential namespace-first or assessment work queue order when packet discovery is still unusable, treats `EXAMPLE_MISSING`, `EXAMPLE_LEAD_MISSING`, `EXAMPLE_ADVANCED_LEAD_MISSING`, `FAMILY_ANCHOR_EXAMPLE_MISSING`, `SAMPLE_STRUCTURE_INVALID`, `FAIL_NEW_SKIP_MARKER_INTRODUCED`, `SAMPLE_SKIP_NOT_ALLOWLISTED`, and `INTERIM_ARTIFACT_IN_WORKTREE` queues as core work rather than checkpoints or quality backlog, drives large example and lead queues through a concrete fast-path micro-loop (next item or next 3-5 items → rerun → continue), suppresses progress-table/checkpoint output until the completion contract is clean or a real external blocker is reported, treats premature completion-shaped handoffs as execution-protocol failures while the queue is still dirty, reserves the final `--build-api-model --validate-samples --verify-docfx-build` verification for the real end of the queue, exposes `summary.fullVerificationRan`, `summary.canClaimCompletion`, `summary.remainingWorkItems`, `summary.remainingDiagnosticsByCode`, `summary.newlyIntroducedSkipMarkers`, and `summary.interimArtifacts` as machine-readable final gates, reruns the fast `docfx.cs --json` after edits until the queue is empty, then runs the build-backed verification before completion, preserves manual edits and authored Markdown during cleanup, skips recursive generated-output cleanup when a target directory contains documentation or source files, and returns deterministic exit codes plus `--json` reports (including process counts, phase timings, warning counts, and skip-marker accounting) so CI can gate on real failures instead of AI claims. | -| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated restores. Focused web tests keep `Test` ownership and use `WebApplicationTestFactory`; shared web fixtures use `WebApplicationTest` with `BlockingManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory`; and shared non-web fixtures use `ApplicationTest` with `BlockingManagedApplicationFixture`. Migrations preserve host configuration, lazy start, clients, services, configuration, disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | +| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated restores. Focused web tests keep `Test` ownership and use `WebApplicationTestFactory`; shared web fixtures use `WebApplicationTest` with `BlockingManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory`; and shared non-web fixtures use `ApplicationTest` with `BlockingManagedApplicationFixture`. Web migrations fail closed unless the chosen Codebelt pattern is present, the legacy factory is absent, and test code does not reconstruct the production composition root with its own `WebApplication` or `TestServer`. Migrations preserve host configuration, lazy start, clients, services, configuration, disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | | [dotnet-benchmark](skills/dotnet-benchmark/SKILL.md) | Discovers, prioritizes, and authors trustworthy BenchmarkDotNet experiments for a .NET type following codebelt conventions and using the `Codebelt.Extensions.BenchmarkDotNet.Console` runner. It inspects implementation code, call sites, tests, existing benchmarks, and available profiles instead of benchmarking every public member; ranks likely high-impact operations; selects representative typical, boundary, scaling, and adverse cases; and rejects external-I/O or service-level questions that need profiling, macrobenchmarks, or load tests. It creates fair current-versus-candidate comparisons only when observable work is equivalent, uses baseline-free single-operation characterization when no honest comparator exists, prevents unrelated construction/formatting/equality/hash ratios, requires exact per-case correctness oracles plus a semantic preflight for truthful workload labels, hard-gates interpretation on a complete valid BenchmarkDotNet summary, preserves workload invariants such as selectivity and hit/miss ratios as sizes scale, distinguishes deferred pipeline creation from terminal/materialization work, and performs Release build, discovery listing, and dry execution before any explicit full run. Explicit `yolo` mode auto-accepts routine repo-derived defaults and the proposed plan, then proceeds through build/list/dry validation without confirmation churn; only a separate explicit human instruction can start a full performance run. Its runner preflight recognizes the standard Slim/runtime setup and explains when `SkipBenchmarksWithReports = true` plus a matching `reports/tuning/` artifact deliberately filters a benchmark, preventing needless class renames, disassembly, or tool thrash; after the first valid full result it stops unless deeper diagnostics could change a real engineering decision. Harness setup remains adaptive: it detects `.slnx`/`.sln`, CPM, existing `tuning/` projects, and a reusable `tooling/` runner, onboards only missing pieces, resolves package versions dynamically, and keeps the benchmark class in the SUT namespace. | | [agent-smith](skills/agent-smith/SKILL.md) | Apply a rigorous, consistent, evidence-driven software-craftsmanship standard across a whole engineering task. Invoke explicitly as `/agent-smith ` or let it auto-trigger for design, architecture, implementation, refactoring, code review, public API review, compatibility and Semantic Versioning analysis, testing, benchmarking, performance, skill authoring, documentation, security and DevSecOps, CI/CD, delivery, repository governance, and engineering assessment. Skill-authoring mode grounds instructions in real execution, requires an explicit bounded-concurrency assessment so independent data retrieval and eval work do not remain sequential by habit, favors reusable C#/.NET scripts and validators against the dynamically resolved latest supported LTS when local constraints do not decide, and follows the Agent Skills guidance for progressive disclosure, description optimization, candidate-versus-baseline evaluation, aggregation, and human review. Its optional .NET EditorConfig conformance mode handles targeted IDE/CA diagnostic remediation and full informational-or-higher `dotnet format` conformance without treating a clean build as proof of policy compliance: user-defined diagnostic IDs remain task-supplied data; target, path, and severity scope remains authoritative; informational workflows explicitly preserve `--severity info` because the formatter defaults to `warn`; targeted IDE and analyzer checks use category-specific formatter subcommands; every formatter invocation is read-only via `--verify-no-changes`; `--no-restore` is never treated as a conformance fallback; fixes are deliberate source edits; repeated multi-target findings are de-duplicated by physical file, diagnostic, and span; and the bundled `repair-roslyn-multiproject-artifacts.ps1` detects conflict artifacts independently of diagnostic ID, preflights directory repairs without partial writes, repairs only proven structural patterns, and refuses unrecognized shapes. Completion requires the same scoped formatter gate plus an artifact scan before affected builds and relevant tests. Technology-neutral work remains unaffected. Performs the requested work (not just a review), loads only relevant `references/`, respects repository conventions, scales process depth without lowering the standard, and reports evidence and risk honestly in concise feedback that may sacrifice grammar but never required evidence. Governing principle: consistency is key. | @@ -619,10 +619,11 @@ API documentation rots the moment code changes. A new public type ships without Test-project refactoring is deceptively lifecycle-sensitive. A `WebApplicationFactory` wrapper may own temporary directories, defer host startup until the first client, replace services in a specific order, or isolate settings per test. Console and worker tests have a different boundary: they need a resolvable in-process Generic Host, not a child process hidden behind a test helper. -**dotnet-test** begins with machine-readable inspection, then chooses the Codebelt pattern that matches the selected project's role and ownership model. It preserves package ownership and frameworks, migrates xUnit v2 to v3/Microsoft Testing Platform when needed, and makes zero remaining selected `WebApplicationFactory` usages plus restore/build/test explicit gates. +**dotnet-test** begins with machine-readable inspection, then chooses the Codebelt pattern that matches the selected project's role and ownership model. It preserves package ownership and frameworks, migrates xUnit v2 to v3/Microsoft Testing Platform when needed, and makes the chosen focused/shared web pattern, zero remaining selected `WebApplicationFactory` usages, zero replacement composition roots, and restore/build/test explicit gates. - **Three explicit roles** — ordinary unit, ASP.NET Core functional, and console/worker functional tests route to separate references and assets, - **Lifecycle-preserving web migration** — focused factory and shared blocking-fixture patterns retain configuration, lazy start, client/service access, disposal, and isolation, +- **Real entry-point coverage** — focused and shared postconditions reject test-owned `WebApplication`/`TestServer` pipelines that can pass while the production `Program` is broken, - **Generic Host boundary** — non-web tests use `ApplicationTestFactory` or `ApplicationTest`; missing host seams are reported precisely unless production adaptation is authorized, - **Bootstrapper host fidelity** — Startup-based hosts and `MinimalConsoleProgram`, `MinimalWorkerProgram`, or `MinimalWebProgram` hosts remain in their established family instead of being rewritten for test convenience, - **Dynamic compatibility** — stable package versions come from NuGet and must pass an isolated restore for the selected target frameworks, diff --git a/skills/dotnet-test/SKILL.md b/skills/dotnet-test/SKILL.md index 3801d9a..0809483 100644 --- a/skills/dotnet-test/SKILL.md +++ b/skills/dotnet-test/SKILL.md @@ -14,6 +14,8 @@ Bootstrap and refactor xUnit projects using the tested patterns from [Codebelt x - Inspect before editing. Run `scripts/inspect-dotnet-tests.ps1` against the selected project and treat its role, package ownership, `WebApplicationFactory` inventory, and blockers as the starting contract. - Classify every selected project as exactly one of: **Ordinary unit test**, **ASP.NET Core functional test**, or **Console or worker functional test**. +- Treat the selected Codebelt host abstraction as an output contract. Removing `WebApplicationFactory` is not a migration unless focused web tests use `WebApplicationTestFactory` or shared web tests use `WebApplicationTest`. +- Do not reconstruct an application entry point inside test code with `WebApplication.CreateBuilder`, `WebHostBuilder`, `HostBuilder`, `UseTestServer`, copied service registrations, or copied middleware. That creates a second composition root which can pass while the real `Program` is broken. - Preserve target frameworks, central package management, unrelated MSBuild configuration, existing test names, and test isolation. - Replace every selected `WebApplicationFactory` usage. A partial migration that leaves a selected usage or package reference behind is incomplete. - Keep functional testing in-process. Never add a process-launching fallback for console or worker applications. @@ -79,6 +81,7 @@ Read `references/xunit-v3-modernization.md` whenever inspection reports xUnit v2 - Keep the test class derived from `Test`. - Use `WebApplicationTestFactory.Create(...)` per focused test or per deliberately owned test scope. +- Keep the production entry point as `TEntryPoint`; configure its host through the factory callback instead of building a replacement `WebApplication` in the test project. - Create the client from `application.Host.GetTestClient()` or the returned `TestServer` as appropriate. - Dispose the factory result, clients, responses, and owned external resources at the same effective lifecycle as before. @@ -118,6 +121,8 @@ For any `WebApplicationFactory` migration, read `references/migration-invariants Delete `Microsoft.AspNetCore.Mvc.Testing` only when no selected code or remaining authorized project surface needs it. After edits, search the selected scope for both `WebApplicationFactory` and `Microsoft.AspNetCore.Mvc.Testing`. +A helper may prepare settings or own temporary resources, but the focused test must still call `WebApplicationTestFactory.Create`. The helper must not build, start, stop, or dispose a replacement host, and must not replay statements from `Program`. + ## Step 6: Bootstrap a behavior test Read the selected production source, its public behavior, and nearby tests. Choose the lowest-cost deterministic behavior that could catch a real defect. Adapt the matching asset rather than copying it literally: @@ -134,11 +139,11 @@ Replace every placeholder with repository evidence. Do not invent an endpoint, s Run the narrowest authoritative sequence that covers the selected change: -1. rerun `inspect-dotnet-tests.ps1`; +1. rerun `inspect-dotnet-tests.ps1`; for a web migration, pass `-ExpectedWebPattern Focused` or `-ExpectedWebPattern Shared` to make the chosen pattern, zero legacy factories, and zero direct replacement-host constructions a fail-closed postcondition; 2. restore the selected test project; 3. build the selected test project; 4. run `dotnet test` when restore/build/test was requested and confirm the expected non-zero test count with zero failures; a zero-discovery exit code is a failure and `dotnet run` is not a substitute; -5. for migrations, search the selected scope and confirm zero remaining `WebApplicationFactory` usages; +5. for migrations, search the selected scope and confirm zero remaining `WebApplicationFactory` usages; do not treat that zero count as sufficient without the expected-pattern postcondition; 6. inspect the final diff for target-framework, package-owner, test-name, and unrelated-change drift. If tests expose a migration regression, repair the preserved lifecycle or configuration behavior rather than weakening assertions. diff --git a/skills/dotnet-test/evals/evals.json b/skills/dotnet-test/evals/evals.json index 694e1db..0c09e90 100644 --- a/skills/dotnet-test/evals/evals.json +++ b/skills/dotnet-test/evals/evals.json @@ -44,15 +44,16 @@ }, { "id": 3, - "prompt": "Migrate the attached lean Acme.Cdn.Origin functional tests from the web-cdn-origin-style CdnOriginTestApplication WebApplicationFactory wrapper to the focused Codebelt WebApplicationTestFactory pattern. Preserve per-test settings, Production environment, temporary content ownership, client behavior, services, disposal, isolation, and existing test names. Remove every selected WebApplicationFactory usage and validate restore/build/test.", - "expected_output": "Focused Test-derived functional tests use WebApplicationTestFactory with no WebApplicationFactory remaining and retain per-test isolation/configuration.", + "prompt": "Migrate the attached lean Acme.Cdn.Origin functional tests from the web-cdn-origin-style CdnOriginTestApplication WebApplicationFactory wrapper to the focused Codebelt WebApplicationTestFactory pattern. Preserve per-test settings, Production environment, temporary content ownership, client behavior, services, disposal, isolation, and existing test names. Keep Program as the composition root under test; do not rebuild its WebApplication or TestServer pipeline in test code. Remove every selected WebApplicationFactory usage and validate restore/build/test.", + "expected_output": "Focused Test-derived functional tests bootstrap Program through WebApplicationTestFactory, contain no replacement WebApplication/TestServer composition root or legacy WebApplicationFactory, and retain per-test isolation/configuration.", "expectations": [ "Classifies the project as ASP.NET Core functional tests and selects focused ownership", - "Replaces CdnOriginTestApplication and all WebApplicationFactory usages with WebApplicationTestFactory", + "Replaces CdnOriginTestApplication and all WebApplicationFactory usages with WebApplicationTestFactory.Create", + "Bootstraps the real Program entry point and does not call WebApplication.CreateBuilder, UseTestServer, new TestServer, or otherwise reconstruct the application pipeline in test source", "Preserves Production environment, in-memory settings, temporary content disposal, and one application per test", "Keeps CompressionTest and both existing method names unchanged", "Removes Microsoft.AspNetCore.Mvc.Testing when no longer needed", - "Search finds no WebApplicationFactory in the selected migration and restore/build/test succeed" + "The focused inspector postcondition succeeds, search finds no WebApplicationFactory in the selected migration, and restore/build/test succeed" ], "files": [ "evals/files/focused-web/Directory.Build.props", @@ -111,4 +112,3 @@ } ] } - diff --git a/skills/dotnet-test/references/migration-invariants.md b/skills/dotnet-test/references/migration-invariants.md index 75cf342..a8e103d 100644 --- a/skills/dotnet-test/references/migration-invariants.md +++ b/skills/dotnet-test/references/migration-invariants.md @@ -4,12 +4,15 @@ Before editing, create an inventory for every selected factory type and call sit ## Host construction +- the production entry point remains the composition root under test; - environment name and content root; - configuration sources, order, and key values; - service additions, removals, replacement order, and scopes; - TestServer configuration and any custom host builder behavior; - whether host creation is lazy until `CreateClient`, `Server`, or `Services` is first used. +Do not preserve behavior by copying the production composition root into the test project. `WebApplication.CreateBuilder`, `WebHostBuilder`, `HostBuilder`, `UseTestServer`, copied service registrations, or copied middleware are not substitutes for bootstrapping `TEntryPoint` through the chosen Codebelt abstraction. + ## Client behavior - base address; @@ -31,5 +34,4 @@ Before editing, create an inventory for every selected factory type and call sit Prefer focused `WebApplicationTestFactory` ownership when the old test constructed a factory per method, passed varying settings, or owned temporary resources per test. Prefer `WebApplicationTest<...>` when the old project used `IClassFixture>` and its shared lifecycle is intentional. -After migration, search the authorized scope. Zero selected `WebApplicationFactory` identifiers is a completion gate, but it is not sufficient by itself: restore/build/test must also pass and lifecycle invariants must still hold. - +After migration, search the authorized scope. Zero selected `WebApplicationFactory` identifiers is a completion gate, but it is not sufficient by itself: the chosen Codebelt pattern must be present, direct replacement-host construction must be absent, restore/build/test must pass, and lifecycle invariants must still hold. Enforce the web-pattern checks by rerunning `inspect-dotnet-tests.ps1` with `-ExpectedWebPattern Focused` or `-ExpectedWebPattern Shared`. diff --git a/skills/dotnet-test/references/web-functional-tests.md b/skills/dotnet-test/references/web-functional-tests.md index 1b76a84..2c65feb 100644 --- a/skills/dotnet-test/references/web-functional-tests.md +++ b/skills/dotnet-test/references/web-functional-tests.md @@ -17,6 +17,20 @@ using var client = application.Host.GetTestClient(); This maps naturally from tests that previously created a new `WebApplicationFactory` per method. Keep the test class derived from `Test`. +When a test also owns temporary content or another per-test resource, keep that resource beside the factory rather than replacing the factory with a custom host: + +```csharp +using var content = new TempContent(); +using var application = WebApplicationTestFactory.Create(builder => +{ + builder.UseEnvironment(Environments.Production); + builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(CreateSettings(content.Root))); +}); +using var client = application.Host.GetTestClient(); +``` + +The test must bootstrap the real `Program` entry point. Do not reproduce `Program` with `WebApplication.CreateBuilder`, `UseTestServer`, copied service registrations, or copied middleware in a test helper. Such a helper tests its own composition root and can remain green when the deployed entry point no longer works. A shared helper may prepare configuration values or temporary resources, but the test remains the visible owner of `WebApplicationTestFactory` and the returned host test. + ## Shared xUnit fixture ownership Use `WebApplicationTest` when all tests in a class share one initialized host: @@ -49,3 +63,4 @@ public class HealthTest : WebApplicationTest[A-Za-z_][A-Za-z0-9_]*)[^:\r\n]*:\s*(?[^\{]+)') { $inheritance.Add([pscustomobject]@{ path = $record.path; line = $index + 1; type = $Matches.name; baseTypes = $Matches.base.Trim() }) } @@ -244,6 +265,23 @@ $reports = foreach ($project in $projects) { if ($role -eq 'Console or worker functional test' -and $projectReferences.Count -eq 0 -and $combinedSource -notmatch '\b(ApplicationTestFactory|ApplicationTest<)\b') { $blockers.Add('No referenced executable or existing Codebelt application-test entry point was found for the console/worker functional-test role.') } + if (-not [string]::IsNullOrWhiteSpace($ExpectedWebPattern)) { + if ($role -ne 'ASP.NET Core functional test') { + $blockers.Add("The requested $ExpectedWebPattern web postcondition cannot be applied because the selected project was classified as '$role'.") + } + if ($webUsages.Count -gt 0) { + $blockers.Add('The selected web migration still contains WebApplicationFactory. Removing every selected legacy factory is required.') + } + if ($directWebHostConstructions.Count -gt 0) { + $blockers.Add('The selected web migration constructs a replacement host in test code. Bootstrap the production entry point through the selected Codebelt web-test abstraction instead of replaying Program, WebApplication.CreateBuilder, or TestServer setup.') + } + if ($ExpectedWebPattern -eq 'Focused' -and $focusedWebUsages.Count -eq 0) { + $blockers.Add('The focused web postcondition requires at least one WebApplicationTestFactory.Create call in the selected project.') + } + if ($ExpectedWebPattern -eq 'Shared' -and $sharedWebUsages.Count -eq 0) { + $blockers.Add('The shared web postcondition requires a test class derived from WebApplicationTest in the selected project.') + } + } $recommendations = [System.Collections.Generic.List[string]]::new() if ($xunitGeneration -eq 'v2') { $recommendations.Add('Modernize the selected project to xUnit v3 and Microsoft Testing Platform while preserving target frameworks and package ownership.') } @@ -270,14 +308,23 @@ $reports = foreach ($project in $projects) { packageOwnership = $packages inheritance = @($inheritance | Sort-Object path, line) webApplicationFactoryUsages = @($webUsages | Sort-Object path, line) + focusedWebApplicationTestFactoryUsages = @($focusedWebUsages | Sort-Object path, line) + sharedWebApplicationTestUsages = @($sharedWebUsages | Sort-Object path, line) + directWebHostConstructions = @($directWebHostConstructions | Sort-Object path, line) referencedApplications = @($referencedHosts | Sort-Object path) recommendations = @($recommendations) blockers = @($blockers) } } -[ordered]@{ +$result = [ordered]@{ repoRoot = $repoRootPath + expectedWebPattern = $ExpectedWebPattern projectCount = @($reports).Count projects = @($reports) -} | ConvertTo-Json -Depth 8 +} + +$result | ConvertTo-Json -Depth 8 +if (-not [string]::IsNullOrWhiteSpace($ExpectedWebPattern) -and @($reports | Where-Object { $_.blockers.Count -gt 0 }).Count -gt 0) { + exit 2 +} diff --git a/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 b/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 index 600d074..1a6eaa3 100644 --- a/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 +++ b/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 @@ -84,6 +84,32 @@ System.Console.WriteLine("legacy"); if ($workerProject.referencedApplications[0].hostPattern -ne 'MinimalWorkerProgram') { throw 'Expected MinimalWorkerProgram detection.' } if ($consoleProject.referencedApplications[0].hostPattern -ne 'MinimalConsoleProgram') { throw 'Expected MinimalConsoleProgram detection.' } + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest { public void Create() { using var application = WebApplicationTestFactory.Create(); } } +'@ + $focusedJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Focused + if ($LASTEXITCODE -ne 0) { throw "Focused web postcondition exited with $LASTEXITCODE.`n$($focusedJson -join [Environment]::NewLine)" } + $focusedReport = $focusedJson | ConvertFrom-Json + if ($focusedReport.projects[0].focusedWebApplicationTestFactoryUsages.Count -ne 1) { throw 'Expected one focused WebApplicationTestFactory usage.' } + if ($focusedReport.projects[0].directWebHostConstructions.Count -ne 0) { throw 'Expected no direct host construction in the focused pattern.' } + + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; public class HealthTest { public void Create() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); } } +'@ + $manualJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Focused + if ($LASTEXITCODE -ne 2) { throw "Expected direct host reconstruction to exit with 2, found $LASTEXITCODE." } + $manualReport = $manualJson | ConvertFrom-Json + if ($manualReport.projects[0].directWebHostConstructions.Count -ne 1) { throw 'Expected direct WebApplication and TestServer construction evidence on the fixture source line.' } + if (@($manualReport.projects[0].blockers | Where-Object { $_ -match 'replacement host' }).Count -ne 1) { throw 'Expected the replacement-host blocker.' } + + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest : WebApplicationTest> { public HealthTest(BlockingManagedWebApplicationFixture fixture) : base(fixture) { } } +'@ + $sharedJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Shared + if ($LASTEXITCODE -ne 0) { throw "Shared web postcondition exited with $LASTEXITCODE.`n$($sharedJson -join [Environment]::NewLine)" } + $sharedReport = $sharedJson | ConvertFrom-Json + if ($sharedReport.projects[0].sharedWebApplicationTestUsages.Count -ne 1) { throw 'Expected one shared WebApplicationTest usage.' } + Write-Host 'inspect-dotnet-tests.ps1 regression: PASS' } finally { if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force } diff --git a/skills/dotnet-test/scripts/validate-skill.ps1 b/skills/dotnet-test/scripts/validate-skill.ps1 index c8aec58..4003e24 100644 --- a/skills/dotnet-test/scripts/validate-skill.ps1 +++ b/skills/dotnet-test/scripts/validate-skill.ps1 @@ -20,11 +20,16 @@ foreach ($relative in $required) { } $skill = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'SKILL.md')) -foreach ($needle in @('WebApplicationTestFactory', 'ApplicationTestFactory', 'BlockingManagedWebApplicationFixture', 'BlockingManagedApplicationFixture', 'zero remaining `WebApplicationFactory`')) { +foreach ($needle in @('WebApplicationTestFactory', 'ApplicationTestFactory', 'BlockingManagedWebApplicationFixture', 'BlockingManagedApplicationFixture', 'zero remaining `WebApplicationFactory`', '-ExpectedWebPattern', 'second composition root')) { if (-not $skill.Contains($needle, [System.StringComparison]::Ordinal)) { throw "SKILL.md is missing required contract: $needle" } } if (-not $skill.Contains('An MTP executable run may supplement that gate but never replaces it', [System.StringComparison]::Ordinal)) { throw 'SKILL.md must reject MTP executable substitution for requested dotnet test validation.' } +$evals = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'evals/evals.json')) +foreach ($needle in @('WebApplicationTestFactory.Create', 'WebApplication.CreateBuilder', 'focused inspector postcondition')) { + if (-not $evals.Contains($needle, [System.StringComparison]::Ordinal)) { throw "Focused-web eval is missing regression contract: $needle" } +} + & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'test-inspect-dotnet-tests.ps1') if ($LASTEXITCODE -ne 0) { throw "Inspection regression failed with exit code $LASTEXITCODE." } From 0d17fc864e7d075260a0487ff5ec8b79f7e404ec Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 12:54:26 +0200 Subject: [PATCH 05/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20update=20dotnet-test?= =?UTF-8?q?=20skill=20implementation=20and=20validators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance dotnet-test skill with improved test project inspection, deterministic package resolution, structured parameter collection, comprehensive eval scenarios, and lifecycle-preserving functional test patterns. Update repository validation to enforce managed-fixture requirements and eval scenario coverage for dotnet-test compliance. --- scripts/validate-skill-templates.ps1 | 17 +-- skills/dotnet-test/FORMS.md | 17 ++- skills/dotnet-test/SKILL.md | 17 +-- .../application/FocusedApplicationTest.cs | 3 +- .../application/SharedApplicationTest.cs | 5 +- .../assets/web/FocusedWebApplicationTest.cs | 3 +- .../assets/web/SharedWebApplicationTest.cs | 5 +- skills/dotnet-test/evals/evals.json | 45 ++++++-- .../QueuePumpTest.cs | 18 ++++ .../application-functional-tests.md | 11 +- .../references/migration-invariants.md | 5 +- .../references/web-functional-tests.md | 18 ++-- .../scripts/inspect-dotnet-tests.ps1 | 102 +++++++++++++++++- .../scripts/test-inspect-dotnet-tests.ps1 | 46 +++++++- skills/dotnet-test/scripts/validate-skill.ps1 | 4 +- 15 files changed, 260 insertions(+), 56 deletions(-) create mode 100644 skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/QueuePumpTest.cs diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index d56e8d3..a90f0a9 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -1126,9 +1126,10 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ASP.NET Core functional test' Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Console or worker functional test' Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'WebApplicationTestFactory.Create' - Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'WebApplicationTest>' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'WebApplicationTest>' Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ApplicationTestFactory.Create' - Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ApplicationTest>' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'ApplicationTest>' + Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Never emit them in generated or refactored code.' Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Never add a process-launching fallback' Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'zero remaining `WebApplicationFactory`' Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Do not invent an endpoint, service, configuration key, or expected result.' @@ -1136,8 +1137,10 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### project_selection' Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### operation_mode' Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### test_role' + Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### host_ownership' Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle 'Field: ' - Assert-Contains -Name 'dotnet-test/web-functional-tests.md' -Content $web -Needle 'BlockingManagedWebApplicationFixture' + Assert-Contains -Name 'dotnet-test/web-functional-tests.md' -Content $web -Needle 'ManagedWebApplicationFixture' + Assert-Contains -Name 'dotnet-test/web-functional-tests.md' -Content $web -Needle 'scheduled for removal' Assert-Contains -Name 'dotnet-test/application-functional-tests.md' -Content $application -Needle 'Do not introduce `Process.Start`' foreach ($program in @('MinimalConsoleProgram', 'MinimalWorkerProgram', 'MinimalWebProgram')) { Assert-Contains -Name 'dotnet-test/bootstrapper-hosts.md' -Content $bootstrapper -Needle $program @@ -1148,15 +1151,17 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific Assert-Contains -Name 'dotnet-test/migration-invariants.md' -Content $migration -Needle 'lazy until `CreateClient`, `Server`, or `Services`' Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle '-getProperty:TargetFramework,TargetFrameworks,IsTestProject,OutputType,ManagePackageVersionsCentrally,UseMicrosoftTestingPlatformRunner,RootNamespace' Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'webApplicationFactoryUsages' + Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'expectedApplicationPattern' + Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'hostTestOwnerships' Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'packageOwnership' Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'https://api.nuget.org/v3/index.json' Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'isolated restore passed' $evalObject = $evals | ConvertFrom-Json - if (@($evalObject.evals).Count -ne 5) { - throw "dotnet-test must define exactly five requested paired eval scenarios; found $(@($evalObject.evals).Count)" + if (@($evalObject.evals).Count -ne 6) { + throw "dotnet-test must define exactly six requested paired eval scenarios; found $(@($evalObject.evals).Count)" } - foreach ($needle in @('attached Acme.Calculator fixture', 'xUnit v2 project', 'web-cdn-origin-style', 'IClassFixture>', 'ApplicationTestFactory pattern')) { + foreach ($needle in @('attached Acme.Calculator fixture', 'xUnit v2 project', 'web-cdn-origin-style', 'IClassFixture>', 'ApplicationTestFactory pattern', 'ApplicationTest>')) { Assert-Contains -Name 'dotnet-test/evals/evals.json' -Content $evals -Needle $needle } if (@($fixtureFiles | Where-Object { $_ -match '(^|[\\/])(bin|obj)([\\/]|$)' }).Count -gt 0) { diff --git a/skills/dotnet-test/FORMS.md b/skills/dotnet-test/FORMS.md index 5ab16fa..7f65217 100644 --- a/skills/dotnet-test/FORMS.md +++ b/skills/dotnet-test/FORMS.md @@ -45,10 +45,22 @@ Collect only unresolved fields. Prefer native structured controls when the host - **required:** true - **show_when:** `test_role` is `Console or worker functional test`, or auto-classification reports a missing Generic Host blocker +### host_ownership + +- **type:** single-choice +- **prompt:** Which lifecycle should own the functional-test host? +- **choices:** + - Auto-classify from current factory/fixture usage and isolation requirements (Recommended) + - Focused ownership per test or narrow test harness + - Shared xUnit class fixture +- **default:** Auto-classify from current factory/fixture usage and isolation requirements (Recommended) +- **required:** true +- **show_when:** `test_role` is `ASP.NET Core functional test` or `Console or worker functional test`, and repository evidence does not already decide focused versus shared ownership + ### confirmation - **type:** single-choice -- **prompt:** Apply the summarized project, mode, role, package-owner, and application-scope plan? +- **prompt:** Apply the summarized project, mode, role, host-ownership, package-owner, and application-scope plan? - **choices:** - Yes (Recommended) - No @@ -62,5 +74,4 @@ Collect only unresolved fields. Prefer native structured controls when the host - Present the recommended/default choice first and suffix it with `(Recommended)`. - In plain-text fallback mode, start immediately with `Field: ` and show numbered choices. Do not add a conversational preamble. - If the user leaves a shown computed/default choice blank, accept it and continue. -- After all fields are resolved, summarize the exact project, mode, role, package owner, detected blockers, and application adaptation scope, then ask `confirmation`. - +- After all fields are resolved, summarize the exact project, mode, role, host ownership, package owner, detected blockers, and application adaptation scope, then ask `confirmation`. diff --git a/skills/dotnet-test/SKILL.md b/skills/dotnet-test/SKILL.md index 0809483..b706700 100644 --- a/skills/dotnet-test/SKILL.md +++ b/skills/dotnet-test/SKILL.md @@ -1,7 +1,7 @@ --- name: dotnet-test description: > - Bootstrap or refactor .NET xUnit test projects to Codebelt conventions. Use for unit-test setup, xUnit v2-to-v3 modernization, Microsoft Testing Platform adoption, ASP.NET Core WebApplicationFactory migration, shared web fixtures, and in-process console or worker functional tests. Classify the selected project, preserve existing behavior and test names, resolve compatible stable packages from NuGet, and validate restore/build/test. Do not use for NUnit/MSTest-only work, general production refactoring without a test-project goal, or process-launching end-to-end harnesses. + Bootstrap or refactor .NET xUnit test projects to Codebelt conventions. Use for unit-test setup, xUnit v2-to-v3 modernization, Microsoft Testing Platform adoption, ASP.NET Core WebApplicationFactory migration, entrypoint-owned managed fixtures, reusable functional-test harnesses, and in-process console or worker tests. Classify the selected project, preserve behavior and test names, resolve compatible stable packages from NuGet, and validate restore/build/test. Do not use for NUnit/MSTest-only work, production refactoring without a test-project goal, or process-launching end-to-end harnesses. compatibility: > Requires .NET SDK, PowerShell 7+, and network access to NuGet for dynamic package resolution. --- @@ -15,6 +15,7 @@ Bootstrap and refactor xUnit projects using the tested patterns from [Codebelt x - Inspect before editing. Run `scripts/inspect-dotnet-tests.ps1` against the selected project and treat its role, package ownership, `WebApplicationFactory` inventory, and blockers as the starting contract. - Classify every selected project as exactly one of: **Ordinary unit test**, **ASP.NET Core functional test**, or **Console or worker functional test**. - Treat the selected Codebelt host abstraction as an output contract. Removing `WebApplicationFactory` is not a migration unless focused web tests use `WebApplicationTestFactory` or shared web tests use `WebApplicationTest`. +- Use entrypoint-owned `ManagedWebApplicationFixture` and `ManagedApplicationFixture` for new and migrated functional tests. Do not emit their deprecated blocking variants; they are scheduled for removal. - Do not reconstruct an application entry point inside test code with `WebApplication.CreateBuilder`, `WebHostBuilder`, `HostBuilder`, `UseTestServer`, copied service registrations, or copied middleware. That creates a second composition root which can pass while the real `Program` is broken. - Preserve target frameworks, central package management, unrelated MSBuild configuration, existing test names, and test isolation. - Replace every selected `WebApplicationFactory` usage. A partial migration that leaves a selected usage or package reference behind is incomplete. @@ -80,28 +81,30 @@ Read `references/xunit-v3-modernization.md` whenever inspection reports xUnit v2 ### Focused ASP.NET Core functional tests - Keep the test class derived from `Test`. -- Use `WebApplicationTestFactory.Create(...)` per focused test or per deliberately owned test scope. +- Use `WebApplicationTestFactory.Create(..., new ManagedWebApplicationFixture())` per focused test or per deliberately owned test scope. Pass the managed fixture explicitly so the application entry point owns startup instead of silently taking the factory's deprecated blocking default. - Keep the production entry point as `TEntryPoint`; configure its host through the factory callback instead of building a replacement `WebApplication` in the test project. - Create the client from `application.Host.GetTestClient()` or the returned `TestServer` as appropriate. - Dispose the factory result, clients, responses, and owned external resources at the same effective lifecycle as before. ### Shared ASP.NET Core fixtures -- Derive the test class from `WebApplicationTest>`, or from an established derived fixture type that preserves the same contract. +- Derive the test class from `WebApplicationTest>`, or from an established fixture derived from `ManagedWebApplicationFixture` that preserves entrypoint-owned startup. - Accept the fixture and `ITestOutputHelper` in the constructor and pass both to the base. - Put shared host customization in `ConfigureWebHost` or a narrowly derived fixture when configuration must exist before the first host start. ### Focused console or worker functional tests - Keep the test class derived from `Test`. -- Use `ApplicationTestFactory.Create(...)` and inspect services/configuration through the returned host test. +- Use `ApplicationTestFactory.Create(..., new ManagedApplicationFixture())` and inspect services/configuration through the returned host test. Pass the fixture explicitly so `Main` remains the startup owner. ### Shared console or worker fixtures -- Derive from `ApplicationTest>`, or from an established derived fixture type with the same lifecycle. +- Derive from `ApplicationTest>`, or from an established fixture derived from `ManagedApplicationFixture` with the same lifecycle. - Accept the fixture and `ITestOutputHelper` in the constructor and pass both to the base. - Put host customization in `ConfigureHost`. +Treat `BlockingManagedWebApplicationFixture` and `BlockingManagedApplicationFixture` only as deprecated input to migrate away from. Never emit them in generated or refactored code. + For fresh console or worker applications, read `references/bootstrapper-hosts.md` and adapt the matching assets. Do not substitute a vanilla process runner. Preserve an existing Bootstrapper host family. `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` are valid Generic Host seams; do not convert them to their Startup-based counterparts merely to enable tests. @@ -121,7 +124,7 @@ For any `WebApplicationFactory` migration, read `references/migration-invariants Delete `Microsoft.AspNetCore.Mvc.Testing` only when no selected code or remaining authorized project surface needs it. After edits, search the selected scope for both `WebApplicationFactory` and `Microsoft.AspNetCore.Mvc.Testing`. -A helper may prepare settings or own temporary resources, but the focused test must still call `WebApplicationTestFactory.Create`. The helper must not build, start, stop, or dispose a replacement host, and must not replay statements from `Program`. +A helper may prepare settings and temporary resources. When many focused tests repeat the same application setup, a narrow `Test`-derived harness may own `WebApplicationTestFactory.Create` or `ApplicationTestFactory.Create`, accept `ITestOutputHelper`, expose only the host/client and domain-specific resources the tests need, and preserve one harness instance per intended isolation scope. It must dispose both the returned `IHostTest` and every owned resource through the matching synchronous and asynchronous `Test` disposal hooks. It must not build, start, stop, or dispose a replacement host, and must not replay statements from `Program`. ## Step 6: Bootstrap a behavior test @@ -139,7 +142,7 @@ Replace every placeholder with repository evidence. Do not invent an endpoint, s Run the narrowest authoritative sequence that covers the selected change: -1. rerun `inspect-dotnet-tests.ps1`; for a web migration, pass `-ExpectedWebPattern Focused` or `-ExpectedWebPattern Shared` to make the chosen pattern, zero legacy factories, and zero direct replacement-host constructions a fail-closed postcondition; +1. rerun `inspect-dotnet-tests.ps1`; for a web migration, pass `-ExpectedWebPattern Focused` or `-ExpectedWebPattern Shared`; for a console/worker migration, pass `-ExpectedApplicationPattern Focused` or `-ExpectedApplicationPattern Shared`. These gates require the selected Codebelt pattern with an entrypoint-owned managed fixture and reject legacy factories, deprecated blocking fixtures, and direct replacement-host construction; 2. restore the selected test project; 3. build the selected test project; 4. run `dotnet test` when restore/build/test was requested and confirm the expected non-zero test count with zero failures; a zero-discovery exit code is a failure and `dotnet run` is not a substitute; diff --git a/skills/dotnet-test/assets/application/FocusedApplicationTest.cs b/skills/dotnet-test/assets/application/FocusedApplicationTest.cs index c64d342..c281eb2 100644 --- a/skills/dotnet-test/assets/application/FocusedApplicationTest.cs +++ b/skills/dotnet-test/assets/application/FocusedApplicationTest.cs @@ -17,11 +17,10 @@ public class {BEHAVIOR}Test : Test using var application = ApplicationTestFactory.Create<{ENTRY_POINT}>(builder => { {PRESERVED_HOST_CONFIGURATION} - }); + }, new ManagedApplicationFixture<{ENTRY_POINT}>()); var actual = application.Host.Services.GetRequiredService<{SOURCE_GROUNDED_SERVICE}>(); Assert.Equal({SOURCE_GROUNDED_EXPECTED}, actual.{SOURCE_GROUNDED_MEMBER}); } } - diff --git a/skills/dotnet-test/assets/application/SharedApplicationTest.cs b/skills/dotnet-test/assets/application/SharedApplicationTest.cs index 5151cb7..c6e6ea2 100644 --- a/skills/dotnet-test/assets/application/SharedApplicationTest.cs +++ b/skills/dotnet-test/assets/application/SharedApplicationTest.cs @@ -5,9 +5,9 @@ namespace {APPLICATION_NAMESPACE}; -public class {BEHAVIOR}Test : ApplicationTest<{ENTRY_POINT}, BlockingManagedApplicationFixture<{ENTRY_POINT}>> +public class {BEHAVIOR}Test : ApplicationTest<{ENTRY_POINT}, ManagedApplicationFixture<{ENTRY_POINT}>> { - public {BEHAVIOR}Test(BlockingManagedApplicationFixture<{ENTRY_POINT}> hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public {BEHAVIOR}Test(ManagedApplicationFixture<{ENTRY_POINT}> hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } @@ -24,4 +24,3 @@ protected override void ConfigureHost(IHostBuilder builder) {PRESERVED_SHARED_HOST_CONFIGURATION} } } - diff --git a/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs b/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs index ced10d9..9c1546f 100644 --- a/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs +++ b/skills/dotnet-test/assets/web/FocusedWebApplicationTest.cs @@ -17,7 +17,7 @@ public class {BEHAVIOR}Test : Test using var application = WebApplicationTestFactory.Create<{ENTRY_POINT}>(builder => { {PRESERVED_WEB_HOST_CONFIGURATION} - }); + }, new ManagedWebApplicationFixture<{ENTRY_POINT}>()); using var client = application.Host.GetTestClient(); using var response = await client.GetAsync("{SOURCE_GROUNDED_ROUTE}").ConfigureAwait(false); @@ -25,4 +25,3 @@ public class {BEHAVIOR}Test : Test Assert.Equal({SOURCE_GROUNDED_STATUS}, response.StatusCode); } } - diff --git a/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs b/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs index b72b51a..f1c56ab 100644 --- a/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs +++ b/skills/dotnet-test/assets/web/SharedWebApplicationTest.cs @@ -5,9 +5,9 @@ namespace {APPLICATION_NAMESPACE}; -public class {BEHAVIOR}Test : WebApplicationTest<{ENTRY_POINT}, BlockingManagedWebApplicationFixture<{ENTRY_POINT}>> +public class {BEHAVIOR}Test : WebApplicationTest<{ENTRY_POINT}, ManagedWebApplicationFixture<{ENTRY_POINT}>> { - public {BEHAVIOR}Test(BlockingManagedWebApplicationFixture<{ENTRY_POINT}> hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public {BEHAVIOR}Test(ManagedWebApplicationFixture<{ENTRY_POINT}> hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } @@ -26,4 +26,3 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) {PRESERVED_SHARED_WEB_HOST_CONFIGURATION} } } - diff --git a/skills/dotnet-test/evals/evals.json b/skills/dotnet-test/evals/evals.json index 0c09e90..5cedd8d 100644 --- a/skills/dotnet-test/evals/evals.json +++ b/skills/dotnet-test/evals/evals.json @@ -44,13 +44,14 @@ }, { "id": 3, - "prompt": "Migrate the attached lean Acme.Cdn.Origin functional tests from the web-cdn-origin-style CdnOriginTestApplication WebApplicationFactory wrapper to the focused Codebelt WebApplicationTestFactory pattern. Preserve per-test settings, Production environment, temporary content ownership, client behavior, services, disposal, isolation, and existing test names. Keep Program as the composition root under test; do not rebuild its WebApplication or TestServer pipeline in test code. Remove every selected WebApplicationFactory usage and validate restore/build/test.", - "expected_output": "Focused Test-derived functional tests bootstrap Program through WebApplicationTestFactory, contain no replacement WebApplication/TestServer composition root or legacy WebApplicationFactory, and retain per-test isolation/configuration.", + "prompt": "Migrate the attached lean Acme.Cdn.Origin functional tests from the web-cdn-origin-style CdnOriginTestApplication WebApplicationFactory wrapper to a narrow Test-derived Codebelt harness backed by WebApplicationTestFactory. Preserve the reusable application helper, per-test settings, Production environment, temporary content ownership, client behavior, services, sync/async disposal, isolation, test output, and existing test names. Pass ManagedWebApplicationFixture explicitly so Program owns startup. Keep Program as the composition root under test; do not rebuild its WebApplication or TestServer pipeline in test code. Remove every selected WebApplicationFactory usage and validate restore/build/test.", + "expected_output": "A focused Test-derived harness owns an IHostTest created through WebApplicationTestFactory with an explicit ManagedWebApplicationFixture, contains no replacement WebApplication/TestServer composition root or legacy WebApplicationFactory, and retains per-test isolation/configuration with correct sync/async disposal.", "expectations": [ "Classifies the project as ASP.NET Core functional tests and selects focused ownership", - "Replaces CdnOriginTestApplication and all WebApplicationFactory usages with WebApplicationTestFactory.Create", + "Refactors CdnOriginTestApplication into a narrow Test-derived harness that accepts ITestOutputHelper and owns WebApplicationTestFactory.Create", + "Passes ManagedWebApplicationFixture explicitly so startup remains entrypoint-owned and uses no BlockingManagedWebApplicationFixture", "Bootstraps the real Program entry point and does not call WebApplication.CreateBuilder, UseTestServer, new TestServer, or otherwise reconstruct the application pipeline in test source", - "Preserves Production environment, in-memory settings, temporary content disposal, and one application per test", + "Preserves Production environment, in-memory settings, one application per test, and disposes both IHostTest and temporary content through matching synchronous and asynchronous Test hooks", "Keeps CompressionTest and both existing method names unchanged", "Removes Microsoft.AspNetCore.Mvc.Testing when no longer needed", "The focused inspector postcondition succeeds, search finds no WebApplicationFactory in the selected migration, and restore/build/test succeed" @@ -68,11 +69,11 @@ }, { "id": 4, - "prompt": "Migrate the attached Acme.Status functional-test class from IClassFixture> to the shared Codebelt WebApplicationTest> pattern. Preserve shared fixture lifecycle, Staging environment, configuration, service access, client behavior, and existing test names. Remove every selected WebApplicationFactory usage and run restore/build/test.", - "expected_output": "A shared blocking managed web fixture replaces IClassFixture WebApplicationFactory with preserved host customization and passing tests.", + "prompt": "Migrate the attached Acme.Status functional-test class from IClassFixture> to the shared Codebelt WebApplicationTest> pattern. Preserve shared fixture lifecycle, entrypoint-owned startup, Staging environment, configuration, service access, client behavior, and existing test names. Remove every selected WebApplicationFactory usage and run restore/build/test.", + "expected_output": "A shared entrypoint-owned managed web fixture replaces IClassFixture WebApplicationFactory with preserved host customization and passing tests.", "expectations": [ "Selects the shared fixture pattern rather than focused per-test ownership", - "Uses WebApplicationTest with BlockingManagedWebApplicationFixture and constructor-injected ITestOutputHelper", + "Uses WebApplicationTest with ManagedWebApplicationFixture and constructor-injected ITestOutputHelper, with no BlockingManagedWebApplicationFixture", "Moves Staging/configuration customization into the pre-start shared host customization path", "Preserves StatusTest and its existing test method names", "Removes Microsoft.AspNetCore.Mvc.Testing when unused", @@ -89,12 +90,12 @@ }, { "id": 5, - "prompt": "Bootstrap the attached Acme.QueuePump worker functional-test project using the focused Codebelt ApplicationTestFactory pattern. The application already follows Codebelt Bootstrapper Worker and must stay in-process; do not launch a process. Add a source-grounded test that resolves QueuePumpMarker from the started host, preserve net10.0 and central package management, and run restore/build/test.", - "expected_output": "A buildable focused console/worker functional test uses ApplicationTestFactory against the existing Bootstrapper Generic Host and verifies a real registered service.", + "prompt": "Bootstrap the attached Acme.QueuePump worker functional-test project using the focused Codebelt ApplicationTestFactory pattern with an explicit ManagedApplicationFixture. The application already follows Codebelt Bootstrapper Worker and must stay in-process with entrypoint-owned startup; do not launch a process. Add a source-grounded test that resolves QueuePumpMarker from the started host, preserve net10.0 and central package management, and run restore/build/test.", + "expected_output": "A buildable focused console/worker functional test uses ApplicationTestFactory with ManagedApplicationFixture against the existing Bootstrapper Generic Host and verifies a real registered service.", "expectations": [ "Classifies the project as a console or worker functional test", "Recognizes the existing Codebelt Bootstrapper Worker Generic Host and does not rewrite production startup unnecessarily", - "Uses a Test-derived class with ITestOutputHelper and ApplicationTestFactory", + "Uses a Test-derived class with ITestOutputHelper and ApplicationTestFactory, explicitly passing ManagedApplicationFixture and avoiding BlockingManagedApplicationFixture", "Does not use Process.Start, dotnet run, shell execution, or port polling", "Adds a ShouldResolveMarker_WhenApplicationStarts-style test grounded in QueuePumpMarker registration", "Restore, build, and test succeed" @@ -109,6 +110,30 @@ "evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs", "evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj" ] + }, + { + "id": 6, + "prompt": "Refactor the attached Acme.QueuePump shared worker functional test from the deprecated BlockingManagedApplicationFixture, which is scheduled for removal, to ApplicationTest>. Preserve the shared xUnit fixture lifecycle, existing Bootstrapper Worker entry point, QueuePumpTest and ShouldResolveMarker_WhenApplicationStarts names, service assertion, net10.0, central package ownership, and in-process execution. Run restore/build/test.", + "expected_output": "A shared ApplicationTest uses ManagedApplicationFixture so Program owns startup while the existing worker behavior and lifecycle remain intact.", + "expectations": [ + "Classifies the project as a console or worker functional test with shared ownership", + "Uses ApplicationTest> with constructor-injected fixture and ITestOutputHelper", + "Removes BlockingManagedApplicationFixture and does not introduce process launching or a replacement HostBuilder", + "Preserves QueuePumpTest and ShouldResolveMarker_WhenApplicationStarts names and the QueuePumpMarker assertion", + "Preserves the Bootstrapper Worker production entry point, net10.0, and central package ownership", + "The shared application inspector postcondition and restore/build/test succeed" + ], + "files": [ + "evals/files/worker-functional/Directory.Build.props", + "evals/files/worker-functional/Directory.Packages.props", + "evals/files/worker-functional/src/Acme.QueuePump/Acme.QueuePump.csproj", + "evals/files/worker-functional/src/Acme.QueuePump/Program.cs", + "evals/files/worker-functional/src/Acme.QueuePump/Startup.cs", + "evals/files/worker-functional/src/Acme.QueuePump/QueuePumpMarker.cs", + "evals/files/worker-functional/src/Acme.QueuePump/QueuePumpWorker.cs", + "evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/Acme.QueuePump.FunctionalTests.csproj", + "evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/QueuePumpTest.cs" + ] } ] } diff --git a/skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/QueuePumpTest.cs b/skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/QueuePumpTest.cs new file mode 100644 index 0000000..86f9045 --- /dev/null +++ b/skills/dotnet-test/evals/files/worker-functional/test/Acme.QueuePump.FunctionalTests/QueuePumpTest.cs @@ -0,0 +1,18 @@ +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Acme.QueuePump; + +public class QueuePumpTest : ApplicationTest> +{ + public QueuePumpTest(BlockingManagedApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + { + } + + [Fact] + public void ShouldResolveMarker_WhenApplicationStarts() + { + Assert.Equal("queue-pump", Host.Services.GetRequiredService().Value); + } +} diff --git a/skills/dotnet-test/references/application-functional-tests.md b/skills/dotnet-test/references/application-functional-tests.md index 2b8b100..d6c0607 100644 --- a/skills/dotnet-test/references/application-functional-tests.md +++ b/skills/dotnet-test/references/application-functional-tests.md @@ -10,7 +10,7 @@ Keep the test derived from `Test` and use: using var application = ApplicationTestFactory.Create(builder => { builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(settings)); -}); +}, new ManagedApplicationFixture()); var service = application.Host.Services.GetRequiredService(); ``` @@ -22,11 +22,17 @@ Use this when a test needs its own configured host or isolated state. Derive from: ```csharp -ApplicationTest> +ApplicationTest> ``` Pass the fixture and `ITestOutputHelper` to the base constructor. Override `ConfigureHost(IHostBuilder)` for configuration that must exist before the host starts. +Pass `ManagedApplicationFixture` explicitly for focused tests and use it as the shared fixture type. This keeps startup owned by the application entry point. Migrate any deprecated `BlockingManagedApplicationFixture` input; it is scheduled for removal and is never a valid generated target. + +A repeated focused setup may be encapsulated in a narrow `Test`-derived harness. It must accept `ITestOutputHelper`, retain the `IHostTest`, and dispose that host test plus every owned resource in both the synchronous and asynchronous `Test` disposal hooks. + +After migration, run `inspect-dotnet-tests.ps1` with `-ExpectedApplicationPattern Focused` or `-ExpectedApplicationPattern Shared`. A non-zero exit is a migration failure even when restore, build, and tests pass. + ## Host seam gate Acceptable evidence includes Codebelt Bootstrapper `ConsoleProgram`, `MinimalConsoleProgram`, `WorkerProgram`, or `MinimalWorkerProgram`, or another entry point that builds an `IHost`/`IHostBuilder` discoverable by the Codebelt application host factory. @@ -37,4 +43,3 @@ Do not introduce `Process.Start`, `dotnet run`, shell execution, port polling, o 2. the matching Codebelt Bootstrapper host base; 3. the production files that would need adaptation; 4. the test pattern that becomes available after adaptation. - diff --git a/skills/dotnet-test/references/migration-invariants.md b/skills/dotnet-test/references/migration-invariants.md index a8e103d..99b9cf4 100644 --- a/skills/dotnet-test/references/migration-invariants.md +++ b/skills/dotnet-test/references/migration-invariants.md @@ -10,6 +10,7 @@ Before editing, create an inventory for every selected factory type and call sit - service additions, removals, replacement order, and scopes; - TestServer configuration and any custom host builder behavior; - whether host creation is lazy until `CreateClient`, `Server`, or `Services` is first used. +- whether the application entry point invokes and owns startup; do not treat a host that bypasses `Program.Main` as equivalent merely because endpoint tests pass. Do not preserve behavior by copying the production composition root into the test project. `WebApplication.CreateBuilder`, `WebHostBuilder`, `HostBuilder`, `UseTestServer`, copied service registrations, or copied middleware are not substitutes for bootstrapping `TEntryPoint` through the chosen Codebelt abstraction. @@ -32,6 +33,8 @@ Do not preserve behavior by copying the production composition root into the tes ## Selection rule -Prefer focused `WebApplicationTestFactory` ownership when the old test constructed a factory per method, passed varying settings, or owned temporary resources per test. Prefer `WebApplicationTest<...>` when the old project used `IClassFixture>` and its shared lifecycle is intentional. +Prefer focused `WebApplicationTestFactory` ownership when the old test constructed a factory per method, passed varying settings, or owned temporary resources per test. Repeated focused setup may live in a narrow `Test`-derived harness when that harness preserves the same per-instance isolation and disposes its `IHostTest` and resources correctly. Prefer `WebApplicationTest<...>` when the old project used `IClassFixture>` and its shared lifecycle is intentional. + +For either ownership shape, use the entrypoint-owned `ManagedWebApplicationFixture` and pass it explicitly to factories. Migrate deprecated `BlockingManagedWebApplicationFixture` input; never emit it as a target. After migration, search the authorized scope. Zero selected `WebApplicationFactory` identifiers is a completion gate, but it is not sufficient by itself: the chosen Codebelt pattern must be present, direct replacement-host construction must be absent, restore/build/test must pass, and lifecycle invariants must still hold. Enforce the web-pattern checks by rerunning `inspect-dotnet-tests.ps1` with `-ExpectedWebPattern Focused` or `-ExpectedWebPattern Shared`. diff --git a/skills/dotnet-test/references/web-functional-tests.md b/skills/dotnet-test/references/web-functional-tests.md index 2c65feb..5e1de76 100644 --- a/skills/dotnet-test/references/web-functional-tests.md +++ b/skills/dotnet-test/references/web-functional-tests.md @@ -11,7 +11,7 @@ using var application = WebApplicationTestFactory.Create(builder => { builder.UseEnvironment(Environments.Production); builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(settings)); -}); +}, new ManagedWebApplicationFixture()); using var client = application.Host.GetTestClient(); ``` @@ -25,20 +25,24 @@ using var application = WebApplicationTestFactory.Create(builder => { builder.UseEnvironment(Environments.Production); builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(CreateSettings(content.Root))); -}); +}, new ManagedWebApplicationFixture()); using var client = application.Host.GetTestClient(); ``` -The test must bootstrap the real `Program` entry point. Do not reproduce `Program` with `WebApplication.CreateBuilder`, `UseTestServer`, copied service registrations, or copied middleware in a test helper. Such a helper tests its own composition root and can remain green when the deployed entry point no longer works. A shared helper may prepare configuration values or temporary resources, but the test remains the visible owner of `WebApplicationTestFactory` and the returned host test. +Pass `ManagedWebApplicationFixture` explicitly. Current factory defaults may preserve a deprecated blocking path that does not give `Program.Main` ownership of startup; a passing HTTP assertion does not prove the deployed entry point was exercised. + +The test must bootstrap the real `Program` entry point. Do not reproduce `Program` with `WebApplication.CreateBuilder`, `UseTestServer`, copied service registrations, or copied middleware in a test helper. Such a helper tests its own composition root and can remain green when the deployed entry point no longer works. + +When setup is repeated across many focused tests, a narrow `Test`-derived harness may own the factory result and temporary resources. Accept `ITestOutputHelper`, keep one harness per intended isolation scope, and expose a client/host rather than a second composition root. Override both `OnDisposeManagedResources` and `OnDisposeManagedResourcesAsync`: dispose the `IHostTest` and owned resources in each matching path, then call the base hook. Overriding only the synchronous hook is insufficient when callers use `await using`. ## Shared xUnit fixture ownership Use `WebApplicationTest` when all tests in a class share one initialized host: ```csharp -public class HealthTest : WebApplicationTest> +public class HealthTest : WebApplicationTest> { - public HealthTest(BlockingManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) + public HealthTest(ManagedWebApplicationFixture hostFixture, ITestOutputHelper output) : base(hostFixture, output) { } @@ -49,13 +53,13 @@ public class HealthTest : WebApplicationTest` starts the resolved application host synchronously so `TestServer` is ready after fixture initialization. Configuration must be established before first start; do not rely on per-test mutation of a shared host. +`ManagedWebApplicationFixture` captures the host created by the application entry point and starts the deferred host when it is consumed. Configuration must be established before first consumption; do not rely on per-test mutation of a shared host. Migrate any deprecated `BlockingManagedWebApplicationFixture` input; it is scheduled for removal and is never a valid generated target. ## `WebApplicationFactory` mapping | Existing surface | Codebelt focused mapping | Codebelt shared mapping | |---|---|---| -| `ConfigureWebHost` override | `WebApplicationTestFactory.Create` callback | test `ConfigureWebHost` override or derived blocking fixture | +| `ConfigureWebHost` override | `WebApplicationTestFactory.Create` callback | test `ConfigureWebHost` override or derived managed fixture | | `CreateClient()` | `application.Host.GetTestClient()` | `Host.GetTestClient()` or `Server.CreateClient()` | | `Services` | `application.Host.Services` | `Host.Services` / `Server.Services` | | factory disposal | dispose returned host test | xUnit disposes the class fixture | diff --git a/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 b/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 index 6365bdf..c2863c4 100644 --- a/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 +++ b/skills/dotnet-test/scripts/inspect-dotnet-tests.ps1 @@ -2,7 +2,9 @@ param( [string]$RepoRoot = (Get-Location).Path, [string]$ProjectPath, [ValidateSet('Focused', 'Shared')] - [string]$ExpectedWebPattern + [string]$ExpectedWebPattern, + [ValidateSet('Focused', 'Shared')] + [string]$ExpectedApplicationPattern ) Set-StrictMode -Version Latest @@ -135,6 +137,12 @@ $repoRootPath = (Resolve-Path -LiteralPath $RepoRoot).Path if (-not [string]::IsNullOrWhiteSpace($ExpectedWebPattern) -and [string]::IsNullOrWhiteSpace($ProjectPath)) { throw 'ExpectedWebPattern requires one selected ProjectPath so unrelated test projects cannot affect the postcondition.' } +if (-not [string]::IsNullOrWhiteSpace($ExpectedApplicationPattern) -and [string]::IsNullOrWhiteSpace($ProjectPath)) { + throw 'ExpectedApplicationPattern requires one selected ProjectPath so unrelated test projects cannot affect the postcondition.' +} +if (-not [string]::IsNullOrWhiteSpace($ExpectedWebPattern) -and -not [string]::IsNullOrWhiteSpace($ExpectedApplicationPattern)) { + throw 'ExpectedWebPattern and ExpectedApplicationPattern are mutually exclusive.' +} $projects = if ([string]::IsNullOrWhiteSpace($ProjectPath)) { @(Get-ChildItem -LiteralPath $repoRootPath -Recurse -File -Filter '*.csproj' | Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } | @@ -191,9 +199,29 @@ $reports = foreach ($project in $projects) { $webUsages = [System.Collections.Generic.List[object]]::new() $focusedWebUsages = [System.Collections.Generic.List[object]]::new() $sharedWebUsages = [System.Collections.Generic.List[object]]::new() + $focusedApplicationUsages = [System.Collections.Generic.List[object]]::new() + $sharedApplicationUsages = [System.Collections.Generic.List[object]]::new() + $managedWebFixtureUsages = [System.Collections.Generic.List[object]]::new() + $blockingWebFixtureUsages = [System.Collections.Generic.List[object]]::new() + $managedApplicationFixtureUsages = [System.Collections.Generic.List[object]]::new() + $blockingApplicationFixtureUsages = [System.Collections.Generic.List[object]]::new() + $hostTestOwnerships = [System.Collections.Generic.List[object]]::new() $directWebHostConstructions = [System.Collections.Generic.List[object]]::new() $inheritance = [System.Collections.Generic.List[object]]::new() foreach ($record in $sourceRecords) { + $hostTestFields = [regex]::Matches($record.text, '\bIHostTest\s+(?_[A-Za-z_][A-Za-z0-9_]*)\s*(?:[;=])') + foreach ($fieldMatch in $hostTestFields) { + $fieldName = $fieldMatch.Groups['name'].Value + $escapedFieldName = [regex]::Escape($fieldName) + $hostTestOwnerships.Add([pscustomobject]@{ + path = $record.path + field = $fieldName + synchronousDispose = $record.text -match "${escapedFieldName}\s*\.\s*Dispose\s*\(" + asynchronousDispose = $record.text -match "${escapedFieldName}\s*\.\s*DisposeAsync\s*\(" + synchronousHook = $record.text -match '\bOnDisposeManagedResources\s*\(' + asynchronousHook = $record.text -match '\bOnDisposeManagedResourcesAsync\s*\(' + }) + } for ($index = 0; $index -lt $record.lines.Count; $index++) { $line = $record.lines[$index] if ($line -match '\bWebApplicationFactory(?:\s*<|\b)') { @@ -205,6 +233,24 @@ $reports = foreach ($project in $projects) { if ($line -match ':\s*WebApplicationTest\s*<') { $sharedWebUsages.Add([pscustomobject]@{ path = $record.path; line = $index + 1; text = $line.Trim() }) } + if ($line -match '\bApplicationTestFactory\s*\.\s*Create\s*<') { + $focusedApplicationUsages.Add([pscustomobject]@{ path = $record.path; line = $index + 1; text = $line.Trim() }) + } + if ($line -match ':\s*ApplicationTest\s*<') { + $sharedApplicationUsages.Add([pscustomobject]@{ path = $record.path; line = $index + 1; text = $line.Trim() }) + } + if ($line -match '(? so the application entry point owns startup.') + } + foreach ($ownership in $hostTestOwnerships) { + if (-not $ownership.synchronousDispose -or -not $ownership.asynchronousDispose -or -not $ownership.synchronousHook -or -not $ownership.asynchronousHook) { + $blockers.Add("The focused harness '$($ownership.path)' owns $($ownership.field) but does not dispose it through both synchronous and asynchronous Test disposal hooks.") + } + } if ($ExpectedWebPattern -eq 'Focused' -and $focusedWebUsages.Count -eq 0) { $blockers.Add('The focused web postcondition requires at least one WebApplicationTestFactory.Create call in the selected project.') } @@ -282,15 +339,42 @@ $reports = foreach ($project in $projects) { $blockers.Add('The shared web postcondition requires a test class derived from WebApplicationTest in the selected project.') } } + if (-not [string]::IsNullOrWhiteSpace($ExpectedApplicationPattern)) { + if ($role -ne 'Console or worker functional test') { + $blockers.Add("The requested $ExpectedApplicationPattern application postcondition cannot be applied because the selected project was classified as '$role'.") + } + if ($directWebHostConstructions.Count -gt 0) { + $blockers.Add('The selected application migration constructs a replacement host in test code. Bootstrap the production entry point through the selected Codebelt application-test abstraction instead of replaying Program or Generic Host setup.') + } + if ($blockingApplicationFixtureUsages.Count -gt 0) { + $blockers.Add('The selected application migration still uses deprecated BlockingManagedApplicationFixture. Replace it with entrypoint-owned ManagedApplicationFixture; the blocking type is scheduled for removal.') + } + if ($managedApplicationFixtureUsages.Count -eq 0) { + $blockers.Add('The selected application migration must explicitly use ManagedApplicationFixture so the application entry point owns startup.') + } + foreach ($ownership in $hostTestOwnerships) { + if (-not $ownership.synchronousDispose -or -not $ownership.asynchronousDispose -or -not $ownership.synchronousHook -or -not $ownership.asynchronousHook) { + $blockers.Add("The focused harness '$($ownership.path)' owns $($ownership.field) but does not dispose it through both synchronous and asynchronous Test disposal hooks.") + } + } + if ($ExpectedApplicationPattern -eq 'Focused' -and $focusedApplicationUsages.Count -eq 0) { + $blockers.Add('The focused application postcondition requires at least one ApplicationTestFactory.Create call in the selected project.') + } + if ($ExpectedApplicationPattern -eq 'Shared' -and $sharedApplicationUsages.Count -eq 0) { + $blockers.Add('The shared application postcondition requires a test class derived from ApplicationTest in the selected project.') + } + } $recommendations = [System.Collections.Generic.List[string]]::new() if ($xunitGeneration -eq 'v2') { $recommendations.Add('Modernize the selected project to xUnit v3 and Microsoft Testing Platform while preserving target frameworks and package ownership.') } if ([string]$properties.UseMicrosoftTestingPlatformRunner -ne 'true') { $recommendations.Add('Enable UseMicrosoftTestingPlatformRunner for the selected xUnit v3 test project, preferably in its existing shared test-project property owner.') } if ($webUsages.Count -gt 0) { $recommendations.Add('Replace every selected WebApplicationFactory usage and preserve configuration, start behavior, clients, services, disposal, and isolation.') } + if ($blockingWebFixtureUsages.Count -gt 0) { $recommendations.Add('Replace deprecated BlockingManagedWebApplicationFixture usage with entrypoint-owned ManagedWebApplicationFixture; the blocking type is scheduled for removal.') } + if ($blockingApplicationFixtureUsages.Count -gt 0) { $recommendations.Add('Replace deprecated BlockingManagedApplicationFixture usage with entrypoint-owned ManagedApplicationFixture; the blocking type is scheduled for removal.') } switch ($role) { 'Ordinary unit test' { $recommendations.Add('Use Test or an established Test-derived base with ITestOutputHelper.') } - 'ASP.NET Core functional test' { $recommendations.Add('Use WebApplicationTestFactory for focused ownership or WebApplicationTest with BlockingManagedWebApplicationFixture for shared fixture ownership.') } - 'Console or worker functional test' { $recommendations.Add('Use ApplicationTestFactory for focused ownership or ApplicationTest with BlockingManagedApplicationFixture for shared fixture ownership.') } + 'ASP.NET Core functional test' { $recommendations.Add('Use WebApplicationTestFactory with an explicit ManagedWebApplicationFixture for focused ownership or WebApplicationTest with ManagedWebApplicationFixture for shared fixture ownership.') } + 'Console or worker functional test' { $recommendations.Add('Use ApplicationTestFactory with an explicit ManagedApplicationFixture for focused ownership or ApplicationTest with ManagedApplicationFixture for shared fixture ownership.') } } [pscustomobject]@{ @@ -310,6 +394,13 @@ $reports = foreach ($project in $projects) { webApplicationFactoryUsages = @($webUsages | Sort-Object path, line) focusedWebApplicationTestFactoryUsages = @($focusedWebUsages | Sort-Object path, line) sharedWebApplicationTestUsages = @($sharedWebUsages | Sort-Object path, line) + focusedApplicationTestFactoryUsages = @($focusedApplicationUsages | Sort-Object path, line) + sharedApplicationTestUsages = @($sharedApplicationUsages | Sort-Object path, line) + managedWebApplicationFixtureUsages = @($managedWebFixtureUsages | Sort-Object path, line) + blockingManagedWebApplicationFixtureUsages = @($blockingWebFixtureUsages | Sort-Object path, line) + managedApplicationFixtureUsages = @($managedApplicationFixtureUsages | Sort-Object path, line) + blockingManagedApplicationFixtureUsages = @($blockingApplicationFixtureUsages | Sort-Object path, line) + hostTestOwnerships = @($hostTestOwnerships | Sort-Object path, field) directWebHostConstructions = @($directWebHostConstructions | Sort-Object path, line) referencedApplications = @($referencedHosts | Sort-Object path) recommendations = @($recommendations) @@ -320,11 +411,12 @@ $reports = foreach ($project in $projects) { $result = [ordered]@{ repoRoot = $repoRootPath expectedWebPattern = $ExpectedWebPattern + expectedApplicationPattern = $ExpectedApplicationPattern projectCount = @($reports).Count projects = @($reports) } $result | ConvertTo-Json -Depth 8 -if (-not [string]::IsNullOrWhiteSpace($ExpectedWebPattern) -and @($reports | Where-Object { $_.blockers.Count -gt 0 }).Count -gt 0) { +if ((-not [string]::IsNullOrWhiteSpace($ExpectedWebPattern) -or -not [string]::IsNullOrWhiteSpace($ExpectedApplicationPattern)) -and @($reports | Where-Object { $_.blockers.Count -gt 0 }).Count -gt 0) { exit 2 } diff --git a/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 b/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 index 1a6eaa3..fc96b2b 100644 --- a/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 +++ b/skills/dotnet-test/scripts/test-inspect-dotnet-tests.ps1 @@ -85,14 +85,31 @@ System.Console.WriteLine("legacy"); if ($consoleProject.referencedApplications[0].hostPattern -ne 'MinimalConsoleProgram') { throw 'Expected MinimalConsoleProgram detection.' } Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' -using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest { public void Create() { using var application = WebApplicationTestFactory.Create(); } } +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest { public void Create() { using var application = WebApplicationTestFactory.Create(hostFixture: new ManagedWebApplicationFixture()); } } '@ $focusedJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Focused if ($LASTEXITCODE -ne 0) { throw "Focused web postcondition exited with $LASTEXITCODE.`n$($focusedJson -join [Environment]::NewLine)" } $focusedReport = $focusedJson | ConvertFrom-Json if ($focusedReport.projects[0].focusedWebApplicationTestFactoryUsages.Count -ne 1) { throw 'Expected one focused WebApplicationTestFactory usage.' } + if ($focusedReport.projects[0].managedWebApplicationFixtureUsages.Count -ne 1) { throw 'Expected one explicit managed web fixture usage.' } if ($focusedReport.projects[0].directWebHostConstructions.Count -ne 0) { throw 'Expected no direct host construction in the focused pattern.' } + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest : Test { private readonly IHostTest _application = WebApplicationTestFactory.Create(hostFixture: new ManagedWebApplicationFixture()); protected override void OnDisposeManagedResources() { _application.Dispose(); base.OnDisposeManagedResources(); } protected override async ValueTask OnDisposeManagedResourcesAsync() { await _application.DisposeAsync(); await base.OnDisposeManagedResourcesAsync(); } } +'@ + $harnessJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Focused + if ($LASTEXITCODE -ne 0) { throw "Focused harness postcondition exited with $LASTEXITCODE.`n$($harnessJson -join [Environment]::NewLine)" } + $harnessReport = $harnessJson | ConvertFrom-Json + if ($harnessReport.projects[0].hostTestOwnerships.Count -ne 1) { throw 'Expected one focused harness IHostTest ownership record.' } + + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest : Test { private readonly IHostTest _application = WebApplicationTestFactory.Create(hostFixture: new ManagedWebApplicationFixture()); protected override void OnDisposeManagedResources() { _application.Dispose(); base.OnDisposeManagedResources(); } } +'@ + $leakingHarnessJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Focused + if ($LASTEXITCODE -ne 2) { throw "Expected incomplete focused harness disposal to exit with 2, found $LASTEXITCODE." } + $leakingHarnessReport = $leakingHarnessJson | ConvertFrom-Json + if (@($leakingHarnessReport.projects[0].blockers | Where-Object { $_ -match 'both synchronous and asynchronous' }).Count -ne 1) { throw 'Expected incomplete focused harness disposal blocker.' } + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; public class HealthTest { public void Create() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); } } '@ @@ -103,13 +120,38 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; public if (@($manualReport.projects[0].blockers | Where-Object { $_ -match 'replacement host' }).Count -ne 1) { throw 'Expected the replacement-host blocker.' } Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' -using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest : WebApplicationTest> { public HealthTest(BlockingManagedWebApplicationFixture fixture) : base(fixture) { } } +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest : WebApplicationTest> { public HealthTest(ManagedWebApplicationFixture fixture) : base(fixture) { } } '@ $sharedJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Shared if ($LASTEXITCODE -ne 0) { throw "Shared web postcondition exited with $LASTEXITCODE.`n$($sharedJson -join [Environment]::NewLine)" } $sharedReport = $sharedJson | ConvertFrom-Json if ($sharedReport.projects[0].sharedWebApplicationTestUsages.Count -ne 1) { throw 'Expected one shared WebApplicationTest usage.' } + Write-File -Path (Join-Path $workspace 'test/App.FunctionalTests/HealthTest.cs') -Content @' +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; public class HealthTest : WebApplicationTest> { public HealthTest(BlockingManagedWebApplicationFixture fixture) : base(fixture) { } } +'@ + $blockingWebJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/App.FunctionalTests/App.FunctionalTests.csproj' -ExpectedWebPattern Shared + if ($LASTEXITCODE -ne 2) { throw "Expected deprecated blocking web fixture to exit with 2, found $LASTEXITCODE." } + $blockingWebReport = $blockingWebJson | ConvertFrom-Json + if ($blockingWebReport.projects[0].blockingManagedWebApplicationFixtureUsages.Count -lt 1) { throw 'Expected blocking managed web fixture evidence.' } + + Write-File -Path (Join-Path $workspace 'test/Worker.FunctionalTests/WorkerTest.cs') -Content @' +using Codebelt.Extensions.Xunit.Hosting; public class WorkerTest { public void Create() { using var application = ApplicationTestFactory.Create(hostFixture: new ManagedApplicationFixture()); } } +'@ + $focusedApplicationJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/Worker.FunctionalTests/Worker.FunctionalTests.csproj' -ExpectedApplicationPattern Focused + if ($LASTEXITCODE -ne 0) { throw "Focused application postcondition exited with $LASTEXITCODE.`n$($focusedApplicationJson -join [Environment]::NewLine)" } + $focusedApplicationReport = $focusedApplicationJson | ConvertFrom-Json + if ($focusedApplicationReport.projects[0].focusedApplicationTestFactoryUsages.Count -ne 1) { throw 'Expected one focused ApplicationTestFactory usage.' } + if ($focusedApplicationReport.projects[0].managedApplicationFixtureUsages.Count -ne 1) { throw 'Expected one explicit managed application fixture usage.' } + + Write-File -Path (Join-Path $workspace 'test/Console.FunctionalTests/ConsoleTest.cs') -Content @' +using Codebelt.Extensions.Xunit.Hosting; public class ConsoleTest : ApplicationTest> { public ConsoleTest(ManagedApplicationFixture fixture) : base(fixture) { } } +'@ + $sharedApplicationJson = & pwsh -NoProfile -File $scriptPath -RepoRoot $workspace -ProjectPath 'test/Console.FunctionalTests/Console.FunctionalTests.csproj' -ExpectedApplicationPattern Shared + if ($LASTEXITCODE -ne 0) { throw "Shared application postcondition exited with $LASTEXITCODE.`n$($sharedApplicationJson -join [Environment]::NewLine)" } + $sharedApplicationReport = $sharedApplicationJson | ConvertFrom-Json + if ($sharedApplicationReport.projects[0].sharedApplicationTestUsages.Count -ne 1) { throw 'Expected one shared ApplicationTest usage.' } + Write-Host 'inspect-dotnet-tests.ps1 regression: PASS' } finally { if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force } diff --git a/skills/dotnet-test/scripts/validate-skill.ps1 b/skills/dotnet-test/scripts/validate-skill.ps1 index 4003e24..e4ad5e6 100644 --- a/skills/dotnet-test/scripts/validate-skill.ps1 +++ b/skills/dotnet-test/scripts/validate-skill.ps1 @@ -20,13 +20,13 @@ foreach ($relative in $required) { } $skill = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'SKILL.md')) -foreach ($needle in @('WebApplicationTestFactory', 'ApplicationTestFactory', 'BlockingManagedWebApplicationFixture', 'BlockingManagedApplicationFixture', 'zero remaining `WebApplicationFactory`', '-ExpectedWebPattern', 'second composition root')) { +foreach ($needle in @('WebApplicationTestFactory', 'ApplicationTestFactory', 'ManagedWebApplicationFixture', 'ManagedApplicationFixture', 'zero remaining `WebApplicationFactory`', '-ExpectedWebPattern', '-ExpectedApplicationPattern', 'second composition root')) { if (-not $skill.Contains($needle, [System.StringComparison]::Ordinal)) { throw "SKILL.md is missing required contract: $needle" } } if (-not $skill.Contains('An MTP executable run may supplement that gate but never replaces it', [System.StringComparison]::Ordinal)) { throw 'SKILL.md must reject MTP executable substitution for requested dotnet test validation.' } $evals = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'evals/evals.json')) -foreach ($needle in @('WebApplicationTestFactory.Create', 'WebApplication.CreateBuilder', 'focused inspector postcondition')) { +foreach ($needle in @('WebApplicationTestFactory.Create', 'ManagedWebApplicationFixture', 'WebApplication.CreateBuilder', 'focused inspector postcondition')) { if (-not $evals.Contains($needle, [System.StringComparison]::Ordinal)) { throw "Focused-web eval is missing regression contract: $needle" } } From 69ec2c4b644096af97be8153196e628cf44e36b7 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 12:54:41 +0200 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=92=AC=20finalize=20v0.9.0=20releas?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalize v0.9.0 release notes documenting the dotnet-test skill introduction with comprehensive xUnit migration, test-role classification, managed-fixture patterns, and validation tooling. Update README.md skill inventory, installation snippet, and motivational content for the new dotnet-test capability. --- CHANGELOG.md | 28 ++++++++++++++++++---------- README.md | 6 +++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d00b08..9e023a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.9.0 - 2026-08-02 +## [0.9.0] - 2026-08-09 -### Added - -- `dotnet-test` skill for classifying, bootstrapping, and refactoring xUnit projects across ordinary unit, ASP.NET Core functional, and console/worker functional roles, -- Deterministic .NET test inspection and NuGet-backed compatible package-resolution scripts, structured forms, role references, adaptable Codebelt xUnit/Bootstrapper assets, and five paired evaluation fixtures, -- Repository validation for the `dotnet-test` lifecycle, modernization, Generic Host, fixture, script, asset, and evaluation contracts. -- Explicit recognition and adaptable assets for Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` host families. +This is a minor release introducing the `dotnet-test` skill, a comprehensive xUnit migration and bootstrapping tool for Codebelt conventions. The release classifies test roles (ordinary unit, ASP.NET Core functional, console/worker functional), preserves host configuration and lifecycle semantics, eliminates legacy `WebApplicationFactory` patterns in favor of role-specific managed fixtures, modernizes xUnit v2 to v3/Microsoft Testing Platform, and includes deterministic project inspection, package-version resolution, eval coverage, and repository validation tooling. -### Changed +### Added -- Updated the root skill catalogue, install bundle, installation examples, and rationale documentation for `dotnet-test`. +- `dotnet-test` skill for classifying, bootstrapping, and refactoring xUnit projects across ordinary unit, ASP.NET Core functional, and console/worker functional test roles with refined WebApplicationTestFactory bootstrap pattern documentation ensuring Program composition root preservation without pipeline reconstruction, +- Deterministic .NET test project inspection via `inspect-dotnet-tests.ps1` extracting target frameworks, test project markers, Generic Host resolution, `WebApplicationFactory` usages, expected application patterns, host test ownerships, and package ownership with additional property discovery for role-specific inspector routing, +- `resolve-test-package-versions.ps1` NuGet-backed package-version resolver for compatible xUnit, Microsoft Testing Platform, and supporting packages across stable isolated restores, +- Structured FORMS.md parameter collection for test role selection, operation mode (fresh bootstrap vs. migration), and host ownership determination, +- Six comprehensive eval scenarios covering fresh unit tests, xUnit v2 modernization, focused web functional tests with `WebApplicationTestFactory`, shared web functional tests with managed fixtures, shared non-web functional tests, and worker-service functional tests, with paired `with_skill` and `without_skill` comparison runs and associated fixture projects, with enhanced prompts and postconditions emphasizing focused-inspector bootstrap contract and managed-fixture presence validation, +- Role-specific reference documentation (`unit-tests.md`, `web-functional-tests.md`, `application-functional-tests.md`, `bootstrapper-hosts.md`, `migration-invariants.md`, `xunit-v3-modernization.md`) covering test patterns, fixture lifecycle, Generic Host boundaries, and migration guidance with clarified lifecycle semantics, managed-fixture deprecation timeline, and entrypoint-owned composition-root preservation, +- Codebelt xUnit and bootstrapper asset templates covering focused/shared unit tests, focused/shared web-application tests, ordinary console/worker functional tests, and bootstrapper Program/Startup patterns for `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts, +- Entrypoint-owned managed fixture patterns (`ManagedWebApplicationFixture`, `ManagedApplicationFixture`) as permanent replacements for deprecated blocking-fixture patterns, +- Lifecycle-preserving functional test migration that retains configuration, lazy startup, synchronous/asynchronous disposal, service access, and test isolation while eliminating `WebApplicationFactory` reconstruction patterns, +- Repository validation rules in `scripts/validate-skill-templates.ps1` for dotnet-test coverage, role-specific encoding, managed-fixture presence, deprecated blocking-fixture rejection, bootstrapper host fidelity, and eval scenario count and content verification; updated to expect six paired eval scenarios and validate managed-fixture references instead of deprecated blocking-fixture patterns, +- Enhanced README.md skill inventory, installation snippet, and "Why dotnet-test?" motivational section explaining lifecycle sensitivity, test-role classification, entrypoint-owned managed fixture, synchronous/asynchronous disposal, and host-family boundary preservation. ## [0.8.2] - 2026-08-07 @@ -564,7 +569,10 @@ This is a minor release that introduces two complementary git workflow skills, e - Improved scaffold fidelity with hidden `.bot` asset preservation, explicit UTF-8 and BOM handling, and checks aimed at preventing mojibake or incomplete generated output. -[Unreleased]: https://github.com/codebeltnet/agentic/compare/v0.8.0...HEAD +[Unreleased]: https://github.com/codebeltnet/agentic/compare/v0.9.0...HEAD +[0.9.0]: https://github.com/codebeltnet/agentic/compare/v0.8.2...v0.9.0 +[0.8.2]: https://github.com/codebeltnet/agentic/compare/v0.8.1...v0.8.2 +[0.8.1]: https://github.com/codebeltnet/agentic/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/codebeltnet/agentic/compare/v0.7.5...v0.8.0 [0.7.5]: https://github.com/codebeltnet/agentic/compare/v0.7.4...v0.7.5 [0.7.4]: https://github.com/codebeltnet/agentic/compare/v0.7.3...v0.7.4 diff --git a/README.md b/README.md index 256063c..9d05eee 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ npx skills add https://github.com/codebeltnet/agentic --skill agent-smith | [git-remote-release](skills/git-remote-release/SKILL.md) | Generate GitHub release notes by summarizing all commits and pull requests between two Git tags or branches in a remote GitHub repository. Accepts a compare URL or separate owner/repo, previous ref, and current ref values; falls back to comparing the current branch against the upstream default branch when no input is provided. Produces a human-friendly `## What's Changed` summary with optional GitHub alert blocks, a `Sources:` section preserving PR and commit references, and a full changelog compare link. | | [dotnet-change-impact](skills/dotnet-change-impact/SKILL.md) | Classify .NET library or NuGet package changes and recommend the correct release bump — `Major`, `Minor`, or `Patch` — for both Semantic Versioning (`MAJOR.MINOR.PATCH`) and .NET assembly/file versioning (`Major.Minor.Build.Revision`), grounded in Microsoft's official .NET compatibility rules. Uses the current Git branch by default when no explicit change details or compare range are provided, resolving it against the upstream/default base branch with local read-only git state. Always returns structured behavioral/binary/source/design-time/backwards compatibility reasoning with the recommendation, even when the bump is clear. | | [dotnet-docfx-digest](skills/dotnet-docfx-digest/SKILL.md) | Create and maintain developer-friendly DocFX documentation for .NET public APIs, including repo-wide no-input audits that inspect source, tests, DocFX config, DocFX `build.content` and `build.overwrite` Markdown inputs, namespace pages, and availability includes before asking for clarification, while treating bare direct skill invocations as autonomous repo-wide runs rather than human-driven checkpoint sessions. Enforces the workflow with two bundled .NET 10 file-based scripts resolved from the loaded skill directory, falling back to the repo-managed source path only when present: `scripts/agents.cs` writes an idempotent, marker-bounded DocFX maintenance block into the repository `AGENTS.md`; `scripts/docfx.cs` is **fast and build-free by default** — it validates Markdown, prose, DocFX overwrite layout, namespace overview pages, `Extension Members` tables, decorated receiver signatures such as `IDecorator`, generic method displays such as `As`, purpose-first summaries, and required per-type/extension examples without invoking `dotnet`, `msbuild`, `docfx`, or `gh`, discovering the public API from existing DocFX YAML metadata or a conservative source scan and ending every run with a `[processes] dotnet=0 msbuild=0 docfx=0 gh=0` summary plus per-phase timings. Compilation and network access are strictly opt-in: `--validate-samples` compiles each C# sample in an isolated project while batching all sample projects into one temporary `.slnx` graph build with bounded MSBuild parallelism and scoped references, `--build-api-model` (alias `--strict-api-discovery`) does reflection-backed discovery from compiled metadata via `MetadataLoadContext` through a single scoped `.slnx` graph build, `--verify-docfx-build` runs the DocFX CLI in a temp copy, and `--search-examples` runs `gh` code search. Final verification adapts to available processors and memory, overlaps isolated DocFX work on high-capacity machines, uses a 30-minute child timeout, and emits 10-second `stderr` heartbeats with active phase, workload, runner count, PID, elapsed time, last-output age, and current child output while preserving machine-readable JSON on `stdout`. Honors a single DocFX metadata `TargetFramework` when `--framework` is omitted, collapses C# 14 extension-block compiler containers such as `$...` back to the authored outer static class in both fast DocFX-YAML discovery and build-backed reflection discovery, validates namespace fly-ins that explain the problem solved/when to use/where to start plus example fly-ins before every C# fence, the Codebelt namespace-and-type-folder overwrite layout (`.docfx/api/namespaces/**/*.md` and `.docfx/api/types/**/*.md` under `build.overwrite` only), keeps `--changed-only` validation scoped to affected docs and APIs while still including brand-new untracked overwrite Markdown, uses the root Codebelt `.snk` when present and falls back to `-p:SkipSignAssembly=true` for keyless strong-name build verification, drains child stdout and stderr concurrently to avoid verbose-build deadlocks, writes deterministic `--assessment-queue` Markdown work queues for noisy audits, preserves working URL references unless a verified HTTP 404 justifies removal, treats unexpected new repo-root or DocFX-workspace files that are not known `dotnet-docfx-digest` deliverables as blocking cleanup diagnostics, keeps assessment/manifests/captured output/helper scripts in temp or session storage instead of the target repository, requires a namespace-first pass across the active queue before net-new type/example authoring during full audits, keeps deeper `EXTENSION_METHOD_MISSING` and `EXTENSION_METHOD_SIGNATURE_MISSING` follow-on diagnostics in that same namespace-layer table-repair phase when they appear after `EXTENSION_SECTION_MISSING` drops, preserves existing BOM and line-ending state while flagging actual mojibake instead of creating encoding-only diffs, and leaves generated DocFX YAML metadata untouched unless `--clean-generated-metadata` is explicitly requested (which runs only after the API model is built, never deleting metadata the run relied on). Documents public API only, uses bundled reference docs for overwrite rules, workflow details, and script behavior, keeps authored API overwrite Markdown under `.docfx/api/namespaces/` and `.docfx/api/types/`, moves legacy authored `.docfx/api/*.md` overwrite files there instead of widening the glob to `api/**/*.md`, teaches namespace and API prose to orient newcomers around purpose instead of inventorying contents, prefers inline or small sibling-batch prose repairs over slow per-page worker fan-out, makes examples start from package-ID usage evidence before type/member-only searches and requires each example to introduce the consumer task before the code, allows multi-type Microsoft Learn-style scenario samples when they better explain the consumer workflow, keeps extension-method examples on readable declaring-class type pages under `.docfx/api/types/` instead of synthetic method-UID filenames or namespace pages that mix extra `uid:` / `example:` blocks into the overview, flags weak skip-compile reasons, requires deterministic `.docfx/skip-compile-allowlist.json` entries for any pre-existing approved skip waivers, treats newly introduced or unallowlisted skip markers as fail-level diagnostics that do not suppress compilation, establishes reflection-backed packets with `--build-api-model --project-manifest` before full-run authoring, forces mid-audit continuations to name that manifest or the sequential assessment/namespace-first fallback explicitly, requires those continuations to restate the fast `docfx.cs --json` rerun cadence, the exact final `docfx.cs --build-api-model --validate-samples --verify-docfx-build --json` gate, and the clean JSON completion contract instead of generic “verify later” prose, treats batch size only as rerun cadence rather than permission to stop, runs a completion repair loop that treats every diagnostic as active work regardless of age or volume, treats newly surfaced follow-on diagnostics as the next repair queue instead of a stop point, reruns packet discovery with `--build-api-model --project-manifest` when fast source-scan packets are unnamed or zero-project, falls back to sequential namespace-first or assessment work queue order when packet discovery is still unusable, treats `EXAMPLE_MISSING`, `EXAMPLE_LEAD_MISSING`, `EXAMPLE_ADVANCED_LEAD_MISSING`, `FAMILY_ANCHOR_EXAMPLE_MISSING`, `SAMPLE_STRUCTURE_INVALID`, `FAIL_NEW_SKIP_MARKER_INTRODUCED`, `SAMPLE_SKIP_NOT_ALLOWLISTED`, and `INTERIM_ARTIFACT_IN_WORKTREE` queues as core work rather than checkpoints or quality backlog, drives large example and lead queues through a concrete fast-path micro-loop (next item or next 3-5 items → rerun → continue), suppresses progress-table/checkpoint output until the completion contract is clean or a real external blocker is reported, treats premature completion-shaped handoffs as execution-protocol failures while the queue is still dirty, reserves the final `--build-api-model --validate-samples --verify-docfx-build` verification for the real end of the queue, exposes `summary.fullVerificationRan`, `summary.canClaimCompletion`, `summary.remainingWorkItems`, `summary.remainingDiagnosticsByCode`, `summary.newlyIntroducedSkipMarkers`, and `summary.interimArtifacts` as machine-readable final gates, reruns the fast `docfx.cs --json` after edits until the queue is empty, then runs the build-backed verification before completion, preserves manual edits and authored Markdown during cleanup, skips recursive generated-output cleanup when a target directory contains documentation or source files, and returns deterministic exit codes plus `--json` reports (including process counts, phase timings, warning counts, and skip-marker accounting) so CI can gate on real failures instead of AI claims. | -| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated restores. Focused web tests keep `Test` ownership and use `WebApplicationTestFactory`; shared web fixtures use `WebApplicationTest` with `BlockingManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory`; and shared non-web fixtures use `ApplicationTest` with `BlockingManagedApplicationFixture`. Web migrations fail closed unless the chosen Codebelt pattern is present, the legacy factory is absent, and test code does not reconstruct the production composition root with its own `WebApplication` or `TestServer`. Migrations preserve host configuration, lazy start, clients, services, configuration, disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | +| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated restores. Focused web tests use `WebApplicationTestFactory` with an explicit entrypoint-owned `ManagedWebApplicationFixture`, directly or through a narrow `Test`-derived harness; shared web fixtures use `WebApplicationTest` with `ManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory` with `ManagedApplicationFixture`; and shared non-web fixtures use `ApplicationTest` with `ManagedApplicationFixture`. Deprecated blocking fixtures are migration inputs only and are never emitted because they are scheduled for removal. Functional migrations fail closed unless the chosen Codebelt pattern and managed fixture are present, the legacy or blocking fixture is absent, and test code does not reconstruct the production composition root with its own `WebApplication`, `TestServer`, or `HostBuilder`. Migrations preserve entrypoint-owned startup, host configuration, lazy start, clients, services, configuration, sync/async disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | | [dotnet-benchmark](skills/dotnet-benchmark/SKILL.md) | Discovers, prioritizes, and authors trustworthy BenchmarkDotNet experiments for a .NET type following codebelt conventions and using the `Codebelt.Extensions.BenchmarkDotNet.Console` runner. It inspects implementation code, call sites, tests, existing benchmarks, and available profiles instead of benchmarking every public member; ranks likely high-impact operations; selects representative typical, boundary, scaling, and adverse cases; and rejects external-I/O or service-level questions that need profiling, macrobenchmarks, or load tests. It creates fair current-versus-candidate comparisons only when observable work is equivalent, uses baseline-free single-operation characterization when no honest comparator exists, prevents unrelated construction/formatting/equality/hash ratios, requires exact per-case correctness oracles plus a semantic preflight for truthful workload labels, hard-gates interpretation on a complete valid BenchmarkDotNet summary, preserves workload invariants such as selectivity and hit/miss ratios as sizes scale, distinguishes deferred pipeline creation from terminal/materialization work, and performs Release build, discovery listing, and dry execution before any explicit full run. Explicit `yolo` mode auto-accepts routine repo-derived defaults and the proposed plan, then proceeds through build/list/dry validation without confirmation churn; only a separate explicit human instruction can start a full performance run. Its runner preflight recognizes the standard Slim/runtime setup and explains when `SkipBenchmarksWithReports = true` plus a matching `reports/tuning/` artifact deliberately filters a benchmark, preventing needless class renames, disassembly, or tool thrash; after the first valid full result it stops unless deeper diagnostics could change a real engineering decision. Harness setup remains adaptive: it detects `.slnx`/`.sln`, CPM, existing `tuning/` projects, and a reusable `tooling/` runner, onboards only missing pieces, resolves package versions dynamically, and keeps the benchmark class in the SUT namespace. | | [agent-smith](skills/agent-smith/SKILL.md) | Apply a rigorous, consistent, evidence-driven software-craftsmanship standard across a whole engineering task. Invoke explicitly as `/agent-smith ` or let it auto-trigger for design, architecture, implementation, refactoring, code review, public API review, compatibility and Semantic Versioning analysis, testing, benchmarking, performance, skill authoring, documentation, security and DevSecOps, CI/CD, delivery, repository governance, and engineering assessment. Skill-authoring mode grounds instructions in real execution, requires an explicit bounded-concurrency assessment so independent data retrieval and eval work do not remain sequential by habit, favors reusable C#/.NET scripts and validators against the dynamically resolved latest supported LTS when local constraints do not decide, and follows the Agent Skills guidance for progressive disclosure, description optimization, candidate-versus-baseline evaluation, aggregation, and human review. Its optional .NET EditorConfig conformance mode handles targeted IDE/CA diagnostic remediation and full informational-or-higher `dotnet format` conformance without treating a clean build as proof of policy compliance: user-defined diagnostic IDs remain task-supplied data; target, path, and severity scope remains authoritative; informational workflows explicitly preserve `--severity info` because the formatter defaults to `warn`; targeted IDE and analyzer checks use category-specific formatter subcommands; every formatter invocation is read-only via `--verify-no-changes`; `--no-restore` is never treated as a conformance fallback; fixes are deliberate source edits; repeated multi-target findings are de-duplicated by physical file, diagnostic, and span; and the bundled `repair-roslyn-multiproject-artifacts.ps1` detects conflict artifacts independently of diagnostic ID, preflights directory repairs without partial writes, repairs only proven structural patterns, and refuses unrecognized shapes. Completion requires the same scoped formatter gate plus an artifact scan before affected builds and relevant tests. Technology-neutral work remains unaffected. Performs the requested work (not just a review), loads only relevant `references/`, respects repository conventions, scales process depth without lowering the standard, and reports evidence and risk honestly in concise feedback that may sacrifice grammar but never required evidence. Governing principle: consistency is key. | @@ -621,10 +621,10 @@ API documentation rots the moment code changes. A new public type ships without Test-project refactoring is deceptively lifecycle-sensitive. A `WebApplicationFactory` wrapper may own temporary directories, defer host startup until the first client, replace services in a specific order, or isolate settings per test. Console and worker tests have a different boundary: they need a resolvable in-process Generic Host, not a child process hidden behind a test helper. -**dotnet-test** begins with machine-readable inspection, then chooses the Codebelt pattern that matches the selected project's role and ownership model. It preserves package ownership and frameworks, migrates xUnit v2 to v3/Microsoft Testing Platform when needed, and makes the chosen focused/shared web pattern, zero remaining selected `WebApplicationFactory` usages, zero replacement composition roots, and restore/build/test explicit gates. +**dotnet-test** begins with machine-readable inspection, then chooses the Codebelt pattern that matches the selected project's role and ownership model. It preserves package ownership and frameworks, migrates xUnit v2 to v3/Microsoft Testing Platform when needed, and makes the chosen focused/shared web or application pattern, entrypoint-owned managed fixture, zero remaining selected `WebApplicationFactory` usages, zero deprecated blocking fixtures, zero replacement composition roots, and restore/build/test explicit gates. - **Three explicit roles** — ordinary unit, ASP.NET Core functional, and console/worker functional tests route to separate references and assets, -- **Lifecycle-preserving web migration** — focused factory and shared blocking-fixture patterns retain configuration, lazy start, client/service access, disposal, and isolation, +- **Lifecycle-preserving functional migration** — focused factories or narrow `Test`-derived harnesses and shared managed fixtures retain configuration, lazy start, client/service access, synchronous/asynchronous disposal, and isolation, - **Real entry-point coverage** — focused and shared postconditions reject test-owned `WebApplication`/`TestServer` pipelines that can pass while the production `Program` is broken, - **Generic Host boundary** — non-web tests use `ApplicationTestFactory` or `ApplicationTest`; missing host seams are reported precisely unless production adaptation is authorized, - **Bootstrapper host fidelity** — Startup-based hosts and `MinimalConsoleProgram`, `MinimalWorkerProgram`, or `MinimalWebProgram` hosts remain in their established family instead of being rewritten for test convenience, From 087527231254d09eb0d9f0e94b52c11c221aa437 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 14:32:12 +0200 Subject: [PATCH 07/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20release-n?= =?UTF-8?q?otes=20skills=20with=20entity=20classification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor git-keep-a-changelog and git-nuget-release-notes skills with improved release-entity classification logic. Add resolve-release-entity.ps1 helper script for deterministic capability classification at release boundaries. Update eval scenarios and documentation to emphasize base-state analysis, eliminate intermediate churn classification, and keep new-capability refinements within Added outcomes. Prevents mis-categorization of new features refined before first release. --- skills/git-keep-a-changelog/SKILL.md | 19 ++- skills/git-keep-a-changelog/evals/evals.json | 14 ++ .../scripts/resolve-release-entity.ps1 | 131 ++++++++++++++++++ .../scripts/test-resolve-release-entity.ps1 | 117 ++++++++++++++++ skills/git-nuget-release-notes/SKILL.md | 10 +- .../git-nuget-release-notes/evals/evals.json | 13 ++ 6 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 create mode 100644 skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 diff --git a/skills/git-keep-a-changelog/SKILL.md b/skills/git-keep-a-changelog/SKILL.md index 2a089c7..e6978d1 100644 --- a/skills/git-keep-a-changelog/SKILL.md +++ b/skills/git-keep-a-changelog/SKILL.md @@ -31,9 +31,11 @@ When the user's request contains `yolo` or `auto` (case-insensitive, anywhere in - If `CHANGELOG.md` does not exist, create a compliant one before populating it. - Read full commit subjects and bodies before writing the changelog. - Inspect the net diff too; do not infer the release from subjects alone. +- Classify each user-facing release entity from whether it existed at the resolved base before considering intermediate commits or individual files. - Treat branch or range topology as the changelog scope source of truth, not author identity. - For branch-derived scope, exclude every commit already reachable from the comparison branch. A merge-base is a boundary, not a release commit. - Run `scripts/resolve-release-scope.ps1` for branch-derived scope and use its emitted ranges without widening them. +- For every path-backed release entity, run `scripts/resolve-release-entity.ps1` with the emitted `merge_base` and `head_commit`; use its classification instead of inferring `Added`, `Removed`, or `Changed` from commit verbs. - Never change range inclusivity because the changelog target is a concrete version instead of `[Unreleased]`. - Include commits from every author/contributor in the selected scope. Do not filter to the current git user, current contributor, bot identity, configured author, or "my changes" unless the user explicitly asks for an author-filtered changelog. - If the current branch starts with a version hint such as `v0.3.0/`, use that to target a concrete release heading. @@ -72,6 +74,8 @@ History is evidence; the resulting state is truth. Reduce first. Interpret second. Summarize last. +Establish the classification baseline at the user-facing release-entity boundary, not independently for every changed file. For a repo-managed skill, the entity is the skill capability together with its dedicated files and inseparable registration, catalog, documentation, validation, and eval wiring. If that entity is absent at the base and present at `HEAD`, its introduction is `Added`; intermediate commits that refine, fix, document, or validate it cannot create `Changed` or `Fixed` outcomes for that same new entity. A change to a separately pre-existing shared capability remains its own outcome and is classified from its own base state. + 1. Inspect cumulative manifest and version deltas across `diff_range`. 2. Inspect the cumulative base-to-`HEAD` diff. 3. Inspect any approved pending worktree changes that are part of the draft. @@ -84,7 +88,7 @@ Do not summarize commits one by one and deduplicate the prose afterward. Classif Reconciliation rules: - Base absent and `HEAD` absent -> omit it. -- Base absent and `HEAD` present -> one surviving `Added` outcome in its final form. +- Release entity absent at base and present at `HEAD` -> one surviving `Added` outcome in its final form. Do not emit `Changed` or `Fixed` outcomes for refinements within that same introduction cycle. - Base present and `HEAD` absent -> one surviving `Removed` outcome. - Base present and identical `HEAD` state -> omit it. - Base present and changed `HEAD` state -> one surviving modification whose section is derived from the final delta. @@ -97,6 +101,7 @@ Examples: - Existing behavior changed and then reverted exactly -> no changelog entry. - File deleted and recreated identically -> no changelog entry. - One capability added, revised, and still present -> usually one `Added` bullet describing its final form, not separate `Added`, `Changed`, and `Fixed` bullets. +- New `skills/dotnet-test/` capability added, then documented, validated, and refined before release -> `Added` only for the complete shipped capability. README registration and validator/eval wiring whose sole purpose is that introduction stay part of the added outcome. ## Release Highlight Contract @@ -290,6 +295,14 @@ Pending changes are additive final-state evidence. They never justify widening ` **4e — Determine the surviving outcomes at the final state.** +- Identify each user-facing release entity and test its existence at the resolved base before classifying its child paths or commit verbs. +- For each path-backed entity, run the bundled classifier. Pass `-IncludeWorktree` only when pending changes for that entity are in the approved scope: + +```powershell +pwsh -NoProfile -File /scripts/resolve-release-entity.ps1 -Repository . -BaseCommit -HeadCommit -EntityPath skills/dotnet-test +``` + +- Treat the emitted `classification` as authoritative for `Added`, `Removed`, `Changed`, or `Unchanged`. Use semantic analysis only to group paths into the correct user-facing entity and to choose among non-structural sections such as `Fixed` or `Security` when the entity already existed at the base. - Eliminate exact reversions, temporary files/features, and dependency churn that returned to the base value. - Merge intermediate add/change/fix churn into the final surviving capability or behavior. - Preserve rename/move as one surviving outcome when the cumulative diff supports it. @@ -323,7 +336,8 @@ Write the release highlight first, then the populated sections. - Map only the surviving outcomes into `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`. - Keep bullets curated and human-written. - Merge overlapping commits into one bullet when they describe the same real outcome. -- A capability that was added, fixed, and refined but survives usually appears once in the section that best describes its final state. +- A capability absent at the base and present at the final state appears under `Added`, even when later commits fixed, documented, validated, or refined it before release. +- Supporting catalog, documentation, validator, and eval changes whose sole purpose is introducing that new capability stay with its `Added` outcome. Classify a shared-file change separately only when it changes a pre-existing capability independently of the new introduction. - A capability, file, or dependency change that returned to the base state stays out of the changelog entirely. - Drop low-signal churn such as typo-only commits, trivial fixups, or mechanical follow-ups unless they materially change the release story. - Use history only for naming, rationale, rename intent, and bug context. Do not let a dramatic commit message manufacture an extra bullet that the final diff does not support. @@ -371,6 +385,7 @@ After updating `CHANGELOG.md`, stop and let the user review the file. Do not com - Copying commit subjects line by line into the changelog. - Reporting temporary features, files, APIs, or dependencies that leave no surviving base-to-`HEAD` change. - Putting one surviving capability under multiple sections because its intermediate commits used different verbs. +- Putting any part of a base-absent capability under `Changed` or `Fixed` because later commits refined, documented, validated, or fixed it before its first release. - Omitting the release highlight. - Failing to classify the release as major, minor, or patch. - Refusing to proceed just because `CHANGELOG.md` does not exist yet. diff --git a/skills/git-keep-a-changelog/evals/evals.json b/skills/git-keep-a-changelog/evals/evals.json index bf76651..da4e933 100644 --- a/skills/git-keep-a-changelog/evals/evals.json +++ b/skills/git-keep-a-changelog/evals/evals.json @@ -214,6 +214,20 @@ "Does not add a Security or other section entry when the final diff contradicts the commit message claim", "Produces a small set of high-signal release outcomes independent of commit count" ] + }, + { + "id": 19, + "prompt": "Create a deterministic temp git repo outside the current repository under `$env:TEMP`, then use git-keep-a-changelog there. Start from a tagged base release containing `CHANGELOG.md`, `README.md`, and `scripts/validate-skills.ps1`, with no `skills/dotnet-test/` directory and no dotnet-test registration. On branch `v0.9.0/dotnet-test`, commit these steps in order: introduce `skills/dotnet-test/SKILL.md`; add its references, assets, scripts, and evals; register it in README.md and the shared validator; refine the new skill twice; fix its new validator expectations; finalize its documentation. Update CHANGELOG.md and stop after the edit.", + "expected_output": "The changelog treats dotnet-test and its inseparable registration, documentation, validation, and eval wiring as one base-absent release entity under Added, with no Changed or Fixed outcome manufactured from its pre-release refinement history.", + "expectations": [ + "Checks whether the dotnet-test release entity existed at the resolved base before classifying its files or commit verbs", + "Runs scripts/resolve-release-entity.ps1 for the path-backed dotnet-test entity and uses its Added classification", + "Classifies the complete dotnet-test capability in its final shipped form under Added", + "Keeps README registration and shared-validator wiring whose sole purpose is the new skill within the Added outcome", + "Does not create a Changed section or Changed bullet for dotnet-test refinements made before its first release", + "Does not create a Fixed section or Fixed bullet for fixes made within the unreleased dotnet-test introduction cycle", + "Would classify an independently changed pre-existing shared capability separately rather than hiding it inside the new skill outcome" + ] } ] } diff --git a/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 b/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 new file mode 100644 index 0000000..6783a07 --- /dev/null +++ b/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 @@ -0,0 +1,131 @@ +[CmdletBinding()] +param( + [Parameter()] + [string] $Repository = '.', + + [Parameter(Mandatory)] + [string] $BaseCommit, + + [Parameter()] + [string] $HeadCommit = 'HEAD', + + [Parameter(Mandatory)] + [string] $EntityPath, + + [Parameter()] + [switch] $IncludeWorktree +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Invoke-Git { + param( + [Parameter(Mandatory)] + [string[]] $Arguments, + + [Parameter()] + [switch] $AllowFailure + ) + + $output = @(& git -C $Repository @Arguments 2>&1) + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0 -and -not $AllowFailure) { + $detail = ($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine + throw "git $($Arguments -join ' ') failed with exit code ${exitCode}: $detail" + } + + [pscustomobject]@{ + ExitCode = $exitCode + Lines = @($output | ForEach-Object { $_.ToString() }) + } +} + +function Resolve-Commit { + param( + [Parameter(Mandatory)] + [string] $Ref + ) + + $result = Invoke-Git -Arguments @('rev-parse', '--verify', '--quiet', "$Ref^{commit}") -AllowFailure + if ($result.ExitCode -ne 0 -or $result.Lines.Count -eq 0) { + throw "Git ref does not resolve to a commit: $Ref" + } + + $result.Lines[0] +} + +function Test-CommitPath { + param( + [Parameter(Mandatory)] + [string] $Commit, + + [Parameter(Mandatory)] + [string] $Path + ) + + (Invoke-Git -Arguments @('cat-file', '-e', "${Commit}:$Path") -AllowFailure).ExitCode -eq 0 +} + +$repositoryRoot = (Invoke-Git -Arguments @('rev-parse', '--show-toplevel')).Lines[0] +$normalizedPath = $EntityPath.Replace('\', '/').Trim('/') +$pathSegments = @($normalizedPath -split '/') + +if ([string]::IsNullOrWhiteSpace($normalizedPath) -or [IO.Path]::IsPathRooted($EntityPath) -or $pathSegments -contains '..') { + throw "EntityPath must be a non-empty repository-relative path without parent traversal: $EntityPath" +} + +$resolvedBase = Resolve-Commit -Ref $BaseCommit +$resolvedHead = Resolve-Commit -Ref $HeadCommit +$baseExists = Test-CommitPath -Commit $resolvedBase -Path $normalizedPath +$headExists = Test-CommitPath -Commit $resolvedHead -Path $normalizedPath +$finalExists = $headExists +$hasDelta = $false + +if ($IncludeWorktree) { + $worktreePath = Join-Path $repositoryRoot ($normalizedPath.Replace('/', [IO.Path]::DirectorySeparatorChar)) + $finalExists = Test-Path -LiteralPath $worktreePath +} + +if ($baseExists -and $finalExists) { + $committedDiff = Invoke-Git -Arguments @('diff', '--quiet', $resolvedBase, $resolvedHead, '--', $normalizedPath) -AllowFailure + if ($committedDiff.ExitCode -notin @(0, 1)) { + throw "Unable to compare entity path between base and HEAD: $normalizedPath" + } + + $hasDelta = $committedDiff.ExitCode -eq 1 + + if ($IncludeWorktree -and -not $hasDelta) { + $worktreeDiff = Invoke-Git -Arguments @('diff', '--quiet', $resolvedHead, '--', $normalizedPath) -AllowFailure + if ($worktreeDiff.ExitCode -notin @(0, 1)) { + throw "Unable to compare pending entity path against HEAD: $normalizedPath" + } + + $untracked = Invoke-Git -Arguments @('ls-files', '--others', '--exclude-standard', '--', $normalizedPath) + $hasDelta = $worktreeDiff.ExitCode -eq 1 -or $untracked.Lines.Count -gt 0 + } +} + +$classification = if (-not $baseExists -and $finalExists) { + 'Added' +} +elseif ($baseExists -and -not $finalExists) { + 'Removed' +} +elseif ($baseExists -and $finalExists -and $hasDelta) { + 'Changed' +} +else { + 'Unchanged' +} + +[pscustomobject]@{ + entity_path = $normalizedPath + base_commit = $resolvedBase + head_commit = $resolvedHead + include_worktree = [bool] $IncludeWorktree + base_exists = $baseExists + final_exists = $finalExists + classification = $classification +} | ConvertTo-Json diff --git a/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 b/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 new file mode 100644 index 0000000..b44dcd0 --- /dev/null +++ b/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 @@ -0,0 +1,117 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$resolver = Join-Path $PSScriptRoot 'resolve-release-entity.ps1' +$testRoot = Join-Path ([IO.Path]::GetTempPath()) ("git-keep-a-changelog-entity-{0}" -f [Guid]::NewGuid().ToString('N')) + +function Invoke-TestGit { + param( + [Parameter(Mandatory)] + [string[]] $Arguments + ) + + $output = @(& git -C $testRoot @Arguments 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed: $($output -join [Environment]::NewLine)" + } + + @($output | ForEach-Object { $_.ToString() }) +} + +function Set-TestFile { + param( + [Parameter(Mandatory)] + [string] $RelativePath, + + [Parameter(Mandatory)] + [string] $Content + ) + + $path = Join-Path $testRoot $RelativePath + $directory = Split-Path -Parent $path + New-Item -ItemType Directory -Path $directory -Force | Out-Null + Set-Content -LiteralPath $path -Value $Content -Encoding utf8NoBOM +} + +function Add-TestCommit { + param( + [Parameter(Mandatory)] + [string] $Message + ) + + Invoke-TestGit -Arguments @('add', '--all') | Out-Null + Invoke-TestGit -Arguments @('-c', 'user.name=Test Author', '-c', 'user.email=test-author@example.invalid', 'commit', '-m', $Message) | Out-Null +} + +function Assert-Classification { + param( + [Parameter(Mandatory)] + [string] $EntityPath, + + [Parameter(Mandatory)] + [string] $Expected, + + [Parameter()] + [switch] $IncludeWorktree + ) + + $parameters = @{ + Repository = $testRoot + BaseCommit = $script:baseCommit + HeadCommit = 'HEAD' + EntityPath = $EntityPath + } + if ($IncludeWorktree) { + $parameters.IncludeWorktree = $true + } + + $result = (& $resolver @parameters | ConvertFrom-Json) + if ($result.classification -ne $Expected) { + throw "Assertion failed for '$EntityPath'. Expected '$Expected', got '$($result.classification)'." + } +} + +try { + New-Item -ItemType Directory -Path $testRoot | Out-Null + Invoke-TestGit -Arguments @('init', '--initial-branch=main') | Out-Null + + Set-TestFile -RelativePath 'skills/existing/SKILL.md' -Content 'existing v1' + Set-TestFile -RelativePath 'skills/legacy/SKILL.md' -Content 'legacy' + Set-TestFile -RelativePath 'README.md' -Content 'catalog' + Add-TestCommit -Message 'base release' + $script:baseCommit = @(Invoke-TestGit -Arguments @('rev-parse', 'HEAD'))[0] + + Invoke-TestGit -Arguments @('switch', '-c', 'v2.0.0/entity-classification') | Out-Null + Set-TestFile -RelativePath 'skills/dotnet-test/SKILL.md' -Content 'initial' + Add-TestCommit -Message 'introduce dotnet-test' + Set-TestFile -RelativePath 'skills/dotnet-test/references/testing.md' -Content 'refined' + Add-TestCommit -Message 'refine dotnet-test' + Set-TestFile -RelativePath 'skills/existing/SKILL.md' -Content 'existing v2' + [IO.File]::Delete((Join-Path $testRoot 'skills/legacy/SKILL.md')) + Add-TestCommit -Message 'change existing and remove legacy' + + Assert-Classification -EntityPath 'skills/dotnet-test' -Expected 'Added' + Assert-Classification -EntityPath 'skills/existing' -Expected 'Changed' + Assert-Classification -EntityPath 'skills/legacy' -Expected 'Removed' + Assert-Classification -EntityPath 'README.md' -Expected 'Unchanged' + + Set-TestFile -RelativePath 'skills/pending/SKILL.md' -Content 'pending' + Assert-Classification -EntityPath 'skills/pending' -Expected 'Unchanged' + Assert-Classification -EntityPath 'skills/pending' -Expected 'Added' -IncludeWorktree + + Write-Output 'PASS: release entities are classified from deterministic base and final-state existence.' +} +finally { + if (Test-Path -LiteralPath $testRoot) { + $resolvedTestRoot = [IO.Path]::GetFullPath($testRoot) + $resolvedTempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()) + if (-not $resolvedTestRoot.StartsWith($resolvedTempRoot, [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove test directory outside the temp root: $resolvedTestRoot" + } + + Remove-Item -LiteralPath $resolvedTestRoot -Recurse -Force + } +} diff --git a/skills/git-nuget-release-notes/SKILL.md b/skills/git-nuget-release-notes/SKILL.md index a2bc532..b91002e 100644 --- a/skills/git-nuget-release-notes/SKILL.md +++ b/skills/git-nuget-release-notes/SKILL.md @@ -20,6 +20,7 @@ Read `references/package-release-notes-format.md` before writing any release-not - For repo-wide requests, every packable `src/` project should end up represented by a corresponding `PackageReleaseNotes.txt` file. - Treat the package's base-to-`HEAD` state as truth; chronological history is supporting provenance. - Inspect cumulative package, API, manifest, version, and metadata deltas before classifying the package history. +- Classify each user-facing package capability from whether it existed at the resolved base before considering intermediate commits or individual files. - Describe only surviving package outcomes. Do not preserve intermediate upgrades, removals, renames, or bug fixes that do not survive into `HEAD`. - Read full commit subjects and bodies before writing the package notes. - Inspect the net diff too; do not classify a package from commit subjects alone. @@ -49,6 +50,8 @@ History is evidence; the resulting state is truth. Reduce first. Interpret second. Summarize last. +Establish the classification baseline at the user-facing package-capability boundary, not independently for every changed file or commit. If a capability is absent at the base and present at `HEAD`, it belongs under `# New Features` with an `ADDED` bullet; intermediate commits that refine, fix, document, or validate that capability cannot move it to `# Improvements` or `# Bug Fixes`. Dependency, TFM, packaging, or separately pre-existing capability changes remain distinct outcomes classified from their own base states. + 1. Inspect cumulative manifest, property, version, and metadata deltas that affect the package. 2. Inspect the cumulative base-to-`HEAD` diff for the package and its shared packaging files. 3. Determine which package changes actually survive at `HEAD`. @@ -61,7 +64,7 @@ Reconciliation rules: - Base state and `HEAD` state are identical -> no entry. - Dependency, API, metadata, or TFM value that returns to the base state -> no entry. -- Base absent and `HEAD` present -> one surviving added capability or surface area outcome. +- Package capability absent at base and present at `HEAD` -> one surviving `ADDED` outcome under `# New Features`. Do not emit `CHANGED`, `EXTENDED`, or `FIXED` outcomes for refinements within that same introduction cycle. - Base present and `HEAD` absent -> one surviving removal. - Base present and changed `HEAD` state -> one surviving modification, fix, rename, or move derived from the final delta. - Equivalent entity/path/name moved or renamed -> one rename/move outcome when the cumulative diff supports it, not add plus remove. @@ -72,7 +75,7 @@ Examples: - `Newtonsoft.Json 13.0.3 -> 14.0.0 -> 14.0.2` -> one surviving upgrade from `13.0.3` to `14.0.2`. - Public API removed and later restored unchanged -> no `# Breaking Changes` bullet. - Feature added, fixed several times, then removed -> no package-note entry for that feature. -- One capability added, reworked, and still present -> usually one final `ADDED`, `CHANGED`, or `EXTENDED` bullet describing what shipped. +- One capability added, reworked, fixed, documented, and still present -> one final `ADDED` bullet under `# New Features` describing what shipped. ## Workflow @@ -152,6 +155,7 @@ For each target package, use this order to understand the real release story. - Inspect cumulative manifest, property, version, and metadata deltas first. This includes `Directory.Packages.props`, package references in project files, `TargetFramework` / `TargetFrameworks`, package metadata, and other shared packaging files that affect the package. - Inspect the cumulative base-to-`HEAD` diff for the package paths. - Determine which package changes survive at `HEAD`: public APIs, dependency versions, TFMs, package metadata, types/members, renames/moves, removals, and bug fixes that still exist. +- Identify each user-facing package capability and test its existence at the resolved base before classifying its child files or commit verbs. - Eliminate exact reversions, temporary features, reverted dependency churn, and restored APIs or metadata that match the base state. - Read the full commit bodies only after the cumulative delta is clear. Use history to explain the surviving outcomes, confirm rename intent, understand migration context, and choose accurate user-facing terminology. Never let an intermediate commit override contradictory final-state evidence. @@ -180,6 +184,7 @@ Classification guidance: Prefer a minimal truthful block over an inflated one. ALM-only releases are valid when the real change was only dependency or TFM maintenance. A restored API or reverted dependency upgrade does not earn a section entry. Use history to help group or explain the surviving outcomes, not to manufacture extra bullets. +Refinement or bug-fix commits made after a capability was first added but before its first release remain part of the `ADDED` new-feature outcome. `# Improvements` and `# Bug Fixes` require the affected capability or behavior to exist at the resolved base. ### Step 7: Write or update PackageReleaseNotes.txt @@ -228,5 +233,6 @@ After updating the relevant `PackageReleaseNotes.txt` files, stop and let the us - Dumping commit subjects line by line into the file. - Reporting temporary dependency, API, metadata, or TFM changes that do not survive into `HEAD`. - Emitting `# Breaking Changes`, `# New Features`, or `# Bug Fixes` bullets for work that was later restored or removed before release. +- Moving a base-absent capability into `# Improvements` or `# Bug Fixes` because intermediate commits refined or fixed it before its first release. - Creating empty headings or filler bullets like "misc updates". - Claiming breaking changes, fixes, or references not supported by git and the project/package metadata. diff --git a/skills/git-nuget-release-notes/evals/evals.json b/skills/git-nuget-release-notes/evals/evals.json index aede57d..52c3b9b 100644 --- a/skills/git-nuget-release-notes/evals/evals.json +++ b/skills/git-nuget-release-notes/evals/evals.json @@ -94,6 +94,19 @@ "Does not report a dependency change that appears only in the commit body when the final diff contradicts it", "Uses history as supporting context only after the final package delta is clear" ] + }, + { + "id": 9, + "prompt": "Create a deterministic temp .NET git repo outside the current repository under `$env:TEMP`, then use git-nuget-release-notes there. Start from a tagged base release with a packable `src/Acme.Core/Acme.Core.csproj`, an existing package release-notes file, and no `RetryPolicy` API. On branch `v2.1.0/retry-policy`, add the public RetryPolicy capability; refine its implementation twice; fix a defect in the new capability; add its tests and documentation; and separately upgrade Newtonsoft.Json from 13.0.3 to 14.0.2. Update the package release notes and stop after the edit.", + "expected_output": "The package notes describe RetryPolicy once as an ADDED New Feature despite its pre-release refinements and fix, while the independent surviving dependency upgrade remains in ALM.", + "expectations": [ + "Checks whether RetryPolicy existed at the resolved base before classifying its files or commit verbs", + "Places RetryPolicy once under New Features with an ADDED action verb", + "Does not place RetryPolicy under Improvements because it was refined before its first release", + "Does not place RetryPolicy under Bug Fixes because a defect was fixed during its introduction cycle", + "Keeps the independent Newtonsoft.Json 13.0.3 to 14.0.2 upgrade under ALM", + "Classifies separately changed pre-existing package capabilities from their own base states" + ] } ] } From fc053e678e4e527b9b399a77e6539bd1629cb53d Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 14:32:28 +0200 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=94=A7=20update=20validation=20and?= =?UTF-8?q?=20documentation=20for=20release=20notes=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update validate-skill-templates.ps1 to enforce release-entity classification contract for git-keep-a-changelog and git-nuget-release-notes. Add changelog trigger validation and resolve-release-entity.ps1 presence checks. Update README.md with release-notes skills inventory reflecting improved classification capability and helper-script integration. --- README.md | 4 ++-- scripts/validate-skill-templates.ps1 | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d05eee..36e4a0b 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,8 @@ npx skills add https://github.com/codebeltnet/agentic --skill agent-smith | Skill | Description | |-------|-------------| | [git-visual-commits](skills/git-visual-commits/SKILL.md) | AI-driven git commit workflow with deterministically validated emoji-first subjects (gitmoji-first), optional conventional prefixes only on explicit request, and three identity modes: bot-attributed (`git bot commit`), human-attributed (`git commit`), and collaborative (`git our commit` — agent analyzes authorship, human picks attribution). Its first critical rule requires a complete SKILL.md read through EOF, then a bundled PowerShell gate rejects unapproved emoji, anything other than one separator space, uppercase description beginnings, and subjects over 70 characters before plan display and immediately before Git. Multi-file plans that initially collapse to one category also require a visible full-context quality gate; one-file changes keep the fast path. Includes commit body by default (opt out with `no-body`), semantic intent splitting, clarification-before-correction safety, and auto-approval mode (`yolo` / `auto`) that cannot bypass validation. Stack-agnostic. | -| [git-keep-a-changelog](skills/git-keep-a-changelog/SKILL.md) | Git-aware Keep a Changelog companion that creates or updates `CHANGELOG.md` from the current branch by default. A bundled deterministic resolver separates branch-unique commit history from merge-base-to-`HEAD` net diffs, excludes the previous-release or comparison boundary for both concrete releases and `[Unreleased]`, and fails if commits already reachable from the comparison branch bleed into the selected scope. It reduces the selected range to surviving base-to-`HEAD` outcomes before section classification, inspects dependency and version manifests before commit bodies, treats the selected branch or explicit range as author-agnostic so all PR contributors remain included, infers a release heading from a branch version hint like `v0.3.0/...`, asks a mandatory `Yes / No / Custom` question before including pending worktree changes in ordinary concrete-release drafts, and keeps yolo/auto limited to automatic staged, unstaged, and untracked inclusion without widening committed history. It also creates missing changelogs, writes required SemVer-aware highlights, maintains compare-link footers, preserves natural prose wrapping, and curates standard Keep a Changelog sections instead of dumping raw commit logs. | -| [git-nuget-release-notes](skills/git-nuget-release-notes/SKILL.md) | Git-aware NuGet release-notes companion for .NET repos that keep cumulative `.nuget/{ProjectName}/PackageReleaseNotes.txt` files. Discovers packable `src/` projects, resolves concrete package version and availability, creates missing files when needed, reduces each package to its surviving base-to-`HEAD` delta before classifying history, and writes per-package `ALM` / `Breaking Changes` / `New Features` / `Improvements` / `Bug Fixes` style notes from final package state plus supporting commit context instead of dumping commit subjects. | +| [git-keep-a-changelog](skills/git-keep-a-changelog/SKILL.md) | Git-aware Keep a Changelog companion that creates or updates `CHANGELOG.md` from the current branch by default. Bundled deterministic resolvers separate branch-unique commit history from merge-base-to-`HEAD` net diffs, exclude the previous-release or comparison boundary, fail on base-history bleed, and classify explicit path-backed release entities as `Added`, `Removed`, `Changed`, or `Unchanged` from their base and final-state existence. The skill reduces the selected range to surviving outcomes before section classification and establishes each user-facing release entity against the base, so a new skill plus its pre-release refinements, documentation, validators, and eval wiring remains one `Added` outcome. It inspects dependency and version manifests before commit bodies, treats the selected branch or explicit range as author-agnostic so all PR contributors remain included, infers a release heading from a branch version hint like `v0.3.0/...`, asks a mandatory `Yes / No / Custom` question before including pending worktree changes in ordinary concrete-release drafts, and keeps yolo/auto limited to automatic staged, unstaged, and untracked inclusion without widening committed history. It also creates missing changelogs, writes required SemVer-aware highlights, maintains compare-link footers, preserves natural prose wrapping, and curates standard Keep a Changelog sections instead of dumping raw commit logs. | +| [git-nuget-release-notes](skills/git-nuget-release-notes/SKILL.md) | Git-aware NuGet release-notes companion for .NET repos that keep cumulative `.nuget/{ProjectName}/PackageReleaseNotes.txt` files. Discovers packable `src/` projects, resolves concrete package version and availability, creates missing files when needed, reduces each package to its surviving base-to-`HEAD` delta before classifying history, and establishes each package capability against the base so pre-release refinements and fixes to a new capability remain one `ADDED` New Feature. It writes per-package `ALM` / `Breaking Changes` / `New Features` / `Improvements` / `Bug Fixes` style notes from final package state plus supporting commit context instead of dumping commit subjects. | | [git-nuget-readme](skills/git-nuget-readme/SKILL.md) | Git-aware NuGet README companion for .NET repos that advertise a package from `src/`. Resolves the real packable project the README should sell, combines git history with actual package metadata, source capabilities, and relevant tests when feasible, preserves honest badge/docs/contributing sections, and writes a forthcoming, adoption-friendly `README.md` with repo-derived branding, clear value, install, framework-support, and quick-start guidance. | | [git-visual-squash-summary](skills/git-visual-squash-summary/SKILL.md) | Non-mutating grouped-summary companion to `git-visual-commits`. Turns the full current feature branch into a curated set of compact lowercase-start summary lines for PR or squash-and-merge contexts by default, comparing against the repository base branch rather than a same-named tracking remote, including commits from all authors unless explicitly narrowed, reducing the cumulative base-to-`HEAD` delta first so reverted churn disappears, preserving technical identifiers, merging overlap, keeping surviving dependency/version changes separate from build/refactor work when the final diff still shows them, and avoiding changelog-style wording, unsupported claims, yolo prompts, needless commit-range questions, or commit-selection UI for ordinary branch-level squash requests. | | [skill-creator-agnostic](skills/skill-creator-agnostic/SKILL.md) | **⚠️ Deprecated** — no longer maintained and retained only for backward compatibility until **1.0.0**. Do not use it for new skill-authoring work; use Anthropic `skill-creator` together with this repository's `AGENTS.md`. | diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index a90f0a9..0f328cb 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -1488,6 +1488,8 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates $evals = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-keep-a-changelog/evals/evals.json' -GitRef $Ref $scopeResolver = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-keep-a-changelog/scripts/resolve-release-scope.ps1' -GitRef $Ref $scopeResolverTests = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-keep-a-changelog/scripts/test-resolve-release-scope.ps1' -GitRef $Ref + $entityResolver = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1' -GitRef $Ref + $entityResolverTests = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1' -GitRef $Ref Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Create or update `CHANGELOG.md` directly, then stop for user review.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'If `CHANGELOG.md` does not exist, create a compliant one before' @@ -1527,6 +1529,10 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-scope.ps1' -Content $scopeResolver -Needle '$diffRange = "$mergeBase..$headCommit"' Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-scope.ps1' -Content $scopeResolver -Needle 'base_history_bleed = $false' Assert-Contains -Name 'git-keep-a-changelog/scripts/test-resolve-release-scope.ps1' -Content $scopeResolverTests -Needle 'the tagged previous release bled into the new release scope' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'run `scripts/resolve-release-entity.ps1` with the emitted `merge_base` and `head_commit`' + Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-entity.ps1' -Content $entityResolver -Needle "'Added'" + Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-entity.ps1' -Content $entityResolver -Needle "'Unchanged'" + Assert-Contains -Name 'git-keep-a-changelog/scripts/test-resolve-release-entity.ps1' -Content $entityResolverTests -Needle "Assert-Classification -EntityPath 'skills/dotnet-test' -Expected 'Added'" Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Updates CHANGELOG.md directly instead of only drafting notes in chat' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Reads full commit subjects and bodies before writing the release entry' @@ -1540,9 +1546,11 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Inserts the compare-link footer at the bottom when it is missing from an existing changelog' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Treats the merge-base as an excluded boundary rather than the first commit of the concrete release' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Does not let yolo mode widen committed history or include the v10.0.9 boundary commit' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Runs scripts/resolve-release-entity.ps1 for the path-backed dotnet-test entity and uses its Added classification' if ([string]::IsNullOrWhiteSpace($Ref)) { & (Join-Path $repoRoot 'skills/git-keep-a-changelog/scripts/test-resolve-release-scope.ps1') | Out-Null + & (Join-Path $repoRoot 'skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1') | Out-Null } } @@ -1558,18 +1566,24 @@ Add-ValidationResult -Results $results -Name 'Git summary skills reduce ranges t Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'History is evidence; the resulting state is truth.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Reduce first. Interpret second. Summarize last.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Base absent and `HEAD` absent -> omit it.' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Classify each user-facing release entity from whether it existed at the resolved base' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'intermediate commits that refine, fix, document, or validate it cannot create `Changed` or `Fixed` outcomes' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Do not summarize commits one by one and deduplicate the prose afterward.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Use history only to explain the surviving outcomes' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Omits `Foo` because it leaves no surviving base-to-HEAD change' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Does not add a Security or other section entry when the final diff contradicts the commit message claim' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Does not create a Changed section or Changed bullet for dotnet-test refinements made before its first release' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'History is evidence; the resulting state is truth.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Classify each user-facing package capability from whether it existed at the resolved base' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle '`# Improvements` and `# Bug Fixes` require the affected capability or behavior to exist at the resolved base.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Do not accumulate bullets from individual commits and deduplicate them afterward.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle '`Newtonsoft.Json 13.0.3 -> 14.0.0 -> 13.0.3` -> no `# ALM` bullet.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Read the full commit bodies only after the cumulative delta is clear.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'A restored API or reverted dependency upgrade does not earn a section entry.' Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Omits the reverted `Newtonsoft.Json` change because the final version matches the base state' Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Does not claim a breaking API removal for `WidgetClient.LegacySend()` because it was restored unchanged' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Does not place RetryPolicy under Improvements because it was refined before its first release' Assert-Contains -Name 'git-visual-squash-summary/SKILL.md' -Content $squashSkill -Needle 'This skill answers one question: **What would this branch effectively do if it were squashed into one commit now?**' Assert-Contains -Name 'git-visual-squash-summary/SKILL.md' -Content $squashSkill -Needle 'History is evidence; the resulting state is truth.' @@ -1580,7 +1594,9 @@ Add-ValidationResult -Results $results -Name 'Git summary skills reduce ranges t Assert-Contains -Name 'git-visual-squash-summary/evals/evals.json' -Content $squashEvals -Needle 'Omits `Foo` entirely because it is absent at both the base and `HEAD` states' Assert-Contains -Name 'README.md' -Content $readme -Needle 'reduces the selected range to surviving base-to-`HEAD` outcomes before section classification' + Assert-Contains -Name 'README.md' -Content $readme -Needle 'establishes each user-facing release entity against the base' Assert-Contains -Name 'README.md' -Content $readme -Needle 'reduces each package to its surviving base-to-`HEAD` delta before classifying history' + Assert-Contains -Name 'README.md' -Content $readme -Needle 'establishes each package capability against the base' Assert-Contains -Name 'README.md' -Content $readme -Needle 'reducing the cumulative base-to-`HEAD` delta first so reverted churn disappears' Assert-Contains -Name 'README.md' -Content $readme -Needle '**Final-state first** — computes the cumulative base-to-`HEAD` delta before reading chronology' } From 972915f055a725a03b5ecdf594fa1fb5e010bc90 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 17:51:55 +0200 Subject: [PATCH 09/16] =?UTF-8?q?=F0=9F=93=9D=20clarify=20combined=20packa?= =?UTF-8?q?ge=20compatibility=20validation=20in=20dotnet-test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKILL.md and evals.json now document the resolver's combined compatibility-project restore behavior: the resolver queries NuGet stable versions, tries newer candidates first, and verifies each candidate against the selected package set through isolated restores. It emits only a set whose combined package restore passes for the target frameworks. --- skills/dotnet-test/SKILL.md | 2 +- skills/dotnet-test/evals/evals.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/dotnet-test/SKILL.md b/skills/dotnet-test/SKILL.md index b706700..a2d45a8 100644 --- a/skills/dotnet-test/SKILL.md +++ b/skills/dotnet-test/SKILL.md @@ -58,7 +58,7 @@ Run the resolver for the selected target frameworks and role: pwsh -NoProfile -File "/scripts/resolve-test-package-versions.ps1" -TargetFramework -Role ``` -The resolver queries NuGet stable versions and verifies candidate compatibility through an isolated restore. If it fails, report the package, target frameworks, and restore evidence instead of guessing. +The resolver queries NuGet stable versions, tries newer candidates first, and verifies each candidate against the selected package set through isolated compatibility-project restores; it emits only a set whose combined package restore passes. If it fails, report the package, target frameworks, and restore evidence instead of guessing. Preserve package ownership: diff --git a/skills/dotnet-test/evals/evals.json b/skills/dotnet-test/evals/evals.json index 5cedd8d..3303209 100644 --- a/skills/dotnet-test/evals/evals.json +++ b/skills/dotnet-test/evals/evals.json @@ -4,11 +4,11 @@ { "id": 1, "prompt": "In the attached Acme.Calculator fixture, bootstrap the existing test project as an ordinary Codebelt xUnit test project. Add a real behavior test for Calculator.Add, preserve net10.0 and central package management, use xUnit v3 with Microsoft Testing Platform, and run restore/build/test.", - "expected_output": "A buildable xUnit v3/MTP unit-test project with Codebelt Test inheritance, ITestOutputHelper, and a source-grounded Add behavior test.", + "expected_output": "A buildable xUnit v3/MTP unit-test project with Codebelt Test inheritance, ITestOutputHelper, a source-grounded Add behavior test, and a NuGet-resolved package set that passes combined compatibility restore.", "expectations": [ "Classifies the selected project as an ordinary unit test", "Keeps net10.0 and central package management", - "Uses xunit.v3, Microsoft Testing Platform, and a compatible Codebelt xUnit package", + "Uses xunit.v3, Microsoft Testing Platform, and a compatible Codebelt xUnit package whose selected package set passes combined compatibility restore", "Adds a Test-derived class with ITestOutputHelper and file-scoped Acme.Calculator namespace", "Adds a ShouldReturnSum_WhenAddingTwoNumbers-style source-grounded behavior test rather than a placeholder", "Restore, build, and test succeed" From 40988120f5457d7f264ad45e28922a3f9879e294 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 17:52:13 +0200 Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=92=AC=20update=20dotnet-test=20ski?= =?UTF-8?q?ll=20capability=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README.md skills table updated to reflect the resolver's new combined compatibility-project restore behavior and combined package set validation. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36e4a0b..28efed9 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ npx skills add https://github.com/codebeltnet/agentic --skill agent-smith | [git-remote-release](skills/git-remote-release/SKILL.md) | Generate GitHub release notes by summarizing all commits and pull requests between two Git tags or branches in a remote GitHub repository. Accepts a compare URL or separate owner/repo, previous ref, and current ref values; falls back to comparing the current branch against the upstream default branch when no input is provided. Produces a human-friendly `## What's Changed` summary with optional GitHub alert blocks, a `Sources:` section preserving PR and commit references, and a full changelog compare link. | | [dotnet-change-impact](skills/dotnet-change-impact/SKILL.md) | Classify .NET library or NuGet package changes and recommend the correct release bump — `Major`, `Minor`, or `Patch` — for both Semantic Versioning (`MAJOR.MINOR.PATCH`) and .NET assembly/file versioning (`Major.Minor.Build.Revision`), grounded in Microsoft's official .NET compatibility rules. Uses the current Git branch by default when no explicit change details or compare range are provided, resolving it against the upstream/default base branch with local read-only git state. Always returns structured behavioral/binary/source/design-time/backwards compatibility reasoning with the recommendation, even when the bump is clear. | | [dotnet-docfx-digest](skills/dotnet-docfx-digest/SKILL.md) | Create and maintain developer-friendly DocFX documentation for .NET public APIs, including repo-wide no-input audits that inspect source, tests, DocFX config, DocFX `build.content` and `build.overwrite` Markdown inputs, namespace pages, and availability includes before asking for clarification, while treating bare direct skill invocations as autonomous repo-wide runs rather than human-driven checkpoint sessions. Enforces the workflow with two bundled .NET 10 file-based scripts resolved from the loaded skill directory, falling back to the repo-managed source path only when present: `scripts/agents.cs` writes an idempotent, marker-bounded DocFX maintenance block into the repository `AGENTS.md`; `scripts/docfx.cs` is **fast and build-free by default** — it validates Markdown, prose, DocFX overwrite layout, namespace overview pages, `Extension Members` tables, decorated receiver signatures such as `IDecorator`, generic method displays such as `As`, purpose-first summaries, and required per-type/extension examples without invoking `dotnet`, `msbuild`, `docfx`, or `gh`, discovering the public API from existing DocFX YAML metadata or a conservative source scan and ending every run with a `[processes] dotnet=0 msbuild=0 docfx=0 gh=0` summary plus per-phase timings. Compilation and network access are strictly opt-in: `--validate-samples` compiles each C# sample in an isolated project while batching all sample projects into one temporary `.slnx` graph build with bounded MSBuild parallelism and scoped references, `--build-api-model` (alias `--strict-api-discovery`) does reflection-backed discovery from compiled metadata via `MetadataLoadContext` through a single scoped `.slnx` graph build, `--verify-docfx-build` runs the DocFX CLI in a temp copy, and `--search-examples` runs `gh` code search. Final verification adapts to available processors and memory, overlaps isolated DocFX work on high-capacity machines, uses a 30-minute child timeout, and emits 10-second `stderr` heartbeats with active phase, workload, runner count, PID, elapsed time, last-output age, and current child output while preserving machine-readable JSON on `stdout`. Honors a single DocFX metadata `TargetFramework` when `--framework` is omitted, collapses C# 14 extension-block compiler containers such as `$...` back to the authored outer static class in both fast DocFX-YAML discovery and build-backed reflection discovery, validates namespace fly-ins that explain the problem solved/when to use/where to start plus example fly-ins before every C# fence, the Codebelt namespace-and-type-folder overwrite layout (`.docfx/api/namespaces/**/*.md` and `.docfx/api/types/**/*.md` under `build.overwrite` only), keeps `--changed-only` validation scoped to affected docs and APIs while still including brand-new untracked overwrite Markdown, uses the root Codebelt `.snk` when present and falls back to `-p:SkipSignAssembly=true` for keyless strong-name build verification, drains child stdout and stderr concurrently to avoid verbose-build deadlocks, writes deterministic `--assessment-queue` Markdown work queues for noisy audits, preserves working URL references unless a verified HTTP 404 justifies removal, treats unexpected new repo-root or DocFX-workspace files that are not known `dotnet-docfx-digest` deliverables as blocking cleanup diagnostics, keeps assessment/manifests/captured output/helper scripts in temp or session storage instead of the target repository, requires a namespace-first pass across the active queue before net-new type/example authoring during full audits, keeps deeper `EXTENSION_METHOD_MISSING` and `EXTENSION_METHOD_SIGNATURE_MISSING` follow-on diagnostics in that same namespace-layer table-repair phase when they appear after `EXTENSION_SECTION_MISSING` drops, preserves existing BOM and line-ending state while flagging actual mojibake instead of creating encoding-only diffs, and leaves generated DocFX YAML metadata untouched unless `--clean-generated-metadata` is explicitly requested (which runs only after the API model is built, never deleting metadata the run relied on). Documents public API only, uses bundled reference docs for overwrite rules, workflow details, and script behavior, keeps authored API overwrite Markdown under `.docfx/api/namespaces/` and `.docfx/api/types/`, moves legacy authored `.docfx/api/*.md` overwrite files there instead of widening the glob to `api/**/*.md`, teaches namespace and API prose to orient newcomers around purpose instead of inventorying contents, prefers inline or small sibling-batch prose repairs over slow per-page worker fan-out, makes examples start from package-ID usage evidence before type/member-only searches and requires each example to introduce the consumer task before the code, allows multi-type Microsoft Learn-style scenario samples when they better explain the consumer workflow, keeps extension-method examples on readable declaring-class type pages under `.docfx/api/types/` instead of synthetic method-UID filenames or namespace pages that mix extra `uid:` / `example:` blocks into the overview, flags weak skip-compile reasons, requires deterministic `.docfx/skip-compile-allowlist.json` entries for any pre-existing approved skip waivers, treats newly introduced or unallowlisted skip markers as fail-level diagnostics that do not suppress compilation, establishes reflection-backed packets with `--build-api-model --project-manifest` before full-run authoring, forces mid-audit continuations to name that manifest or the sequential assessment/namespace-first fallback explicitly, requires those continuations to restate the fast `docfx.cs --json` rerun cadence, the exact final `docfx.cs --build-api-model --validate-samples --verify-docfx-build --json` gate, and the clean JSON completion contract instead of generic “verify later” prose, treats batch size only as rerun cadence rather than permission to stop, runs a completion repair loop that treats every diagnostic as active work regardless of age or volume, treats newly surfaced follow-on diagnostics as the next repair queue instead of a stop point, reruns packet discovery with `--build-api-model --project-manifest` when fast source-scan packets are unnamed or zero-project, falls back to sequential namespace-first or assessment work queue order when packet discovery is still unusable, treats `EXAMPLE_MISSING`, `EXAMPLE_LEAD_MISSING`, `EXAMPLE_ADVANCED_LEAD_MISSING`, `FAMILY_ANCHOR_EXAMPLE_MISSING`, `SAMPLE_STRUCTURE_INVALID`, `FAIL_NEW_SKIP_MARKER_INTRODUCED`, `SAMPLE_SKIP_NOT_ALLOWLISTED`, and `INTERIM_ARTIFACT_IN_WORKTREE` queues as core work rather than checkpoints or quality backlog, drives large example and lead queues through a concrete fast-path micro-loop (next item or next 3-5 items → rerun → continue), suppresses progress-table/checkpoint output until the completion contract is clean or a real external blocker is reported, treats premature completion-shaped handoffs as execution-protocol failures while the queue is still dirty, reserves the final `--build-api-model --validate-samples --verify-docfx-build` verification for the real end of the queue, exposes `summary.fullVerificationRan`, `summary.canClaimCompletion`, `summary.remainingWorkItems`, `summary.remainingDiagnosticsByCode`, `summary.newlyIntroducedSkipMarkers`, and `summary.interimArtifacts` as machine-readable final gates, reruns the fast `docfx.cs --json` after edits until the queue is empty, then runs the build-backed verification before completion, preserves manual edits and authored Markdown during cleanup, skips recursive generated-output cleanup when a target directory contains documentation or source files, and returns deterministic exit codes plus `--json` reports (including process counts, phase timings, warning counts, and skip-marker accounting) so CI can gate on real failures instead of AI claims. | -| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated restores. Focused web tests use `WebApplicationTestFactory` with an explicit entrypoint-owned `ManagedWebApplicationFixture`, directly or through a narrow `Test`-derived harness; shared web fixtures use `WebApplicationTest` with `ManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory` with `ManagedApplicationFixture`; and shared non-web fixtures use `ApplicationTest` with `ManagedApplicationFixture`. Deprecated blocking fixtures are migration inputs only and are never emitted because they are scheduled for removal. Functional migrations fail closed unless the chosen Codebelt pattern and managed fixture are present, the legacy or blocking fixture is absent, and test code does not reconstruct the production composition root with its own `WebApplication`, `TestServer`, or `HostBuilder`. Migrations preserve entrypoint-owned startup, host configuration, lazy start, clients, services, configuration, sync/async disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | +| [dotnet-test](skills/dotnet-test/SKILL.md) | Bootstraps and refactors xUnit projects to Codebelt conventions. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated compatibility-project restores, including the selected combined package set. Focused web tests use `WebApplicationTestFactory` with an explicit entrypoint-owned `ManagedWebApplicationFixture`, directly or through a narrow `Test`-derived harness; shared web fixtures use `WebApplicationTest` with `ManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory` with `ManagedApplicationFixture`; and shared non-web fixtures use `ApplicationTest` with `ManagedApplicationFixture`. Deprecated blocking fixtures are migration inputs only and are never emitted because they are scheduled for removal. Functional migrations fail closed unless the chosen Codebelt pattern and managed fixture are present, the legacy or blocking fixture is absent, and test code does not reconstruct the production composition root with its own `WebApplication`, `TestServer`, or `HostBuilder`. Migrations preserve entrypoint-owned startup, host configuration, lazy start, clients, services, configuration, sync/async disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | | [dotnet-benchmark](skills/dotnet-benchmark/SKILL.md) | Discovers, prioritizes, and authors trustworthy BenchmarkDotNet experiments for a .NET type following codebelt conventions and using the `Codebelt.Extensions.BenchmarkDotNet.Console` runner. It inspects implementation code, call sites, tests, existing benchmarks, and available profiles instead of benchmarking every public member; ranks likely high-impact operations; selects representative typical, boundary, scaling, and adverse cases; and rejects external-I/O or service-level questions that need profiling, macrobenchmarks, or load tests. It creates fair current-versus-candidate comparisons only when observable work is equivalent, uses baseline-free single-operation characterization when no honest comparator exists, prevents unrelated construction/formatting/equality/hash ratios, requires exact per-case correctness oracles plus a semantic preflight for truthful workload labels, hard-gates interpretation on a complete valid BenchmarkDotNet summary, preserves workload invariants such as selectivity and hit/miss ratios as sizes scale, distinguishes deferred pipeline creation from terminal/materialization work, and performs Release build, discovery listing, and dry execution before any explicit full run. Explicit `yolo` mode auto-accepts routine repo-derived defaults and the proposed plan, then proceeds through build/list/dry validation without confirmation churn; only a separate explicit human instruction can start a full performance run. Its runner preflight recognizes the standard Slim/runtime setup and explains when `SkipBenchmarksWithReports = true` plus a matching `reports/tuning/` artifact deliberately filters a benchmark, preventing needless class renames, disassembly, or tool thrash; after the first valid full result it stops unless deeper diagnostics could change a real engineering decision. Harness setup remains adaptive: it detects `.slnx`/`.sln`, CPM, existing `tuning/` projects, and a reusable `tooling/` runner, onboards only missing pieces, resolves package versions dynamically, and keeps the benchmark class in the SUT namespace. | | [agent-smith](skills/agent-smith/SKILL.md) | Apply a rigorous, consistent, evidence-driven software-craftsmanship standard across a whole engineering task. Invoke explicitly as `/agent-smith ` or let it auto-trigger for design, architecture, implementation, refactoring, code review, public API review, compatibility and Semantic Versioning analysis, testing, benchmarking, performance, skill authoring, documentation, security and DevSecOps, CI/CD, delivery, repository governance, and engineering assessment. Skill-authoring mode grounds instructions in real execution, requires an explicit bounded-concurrency assessment so independent data retrieval and eval work do not remain sequential by habit, favors reusable C#/.NET scripts and validators against the dynamically resolved latest supported LTS when local constraints do not decide, and follows the Agent Skills guidance for progressive disclosure, description optimization, candidate-versus-baseline evaluation, aggregation, and human review. Its optional .NET EditorConfig conformance mode handles targeted IDE/CA diagnostic remediation and full informational-or-higher `dotnet format` conformance without treating a clean build as proof of policy compliance: user-defined diagnostic IDs remain task-supplied data; target, path, and severity scope remains authoritative; informational workflows explicitly preserve `--severity info` because the formatter defaults to `warn`; targeted IDE and analyzer checks use category-specific formatter subcommands; every formatter invocation is read-only via `--verify-no-changes`; `--no-restore` is never treated as a conformance fallback; fixes are deliberate source edits; repeated multi-target findings are de-duplicated by physical file, diagnostic, and span; and the bundled `repair-roslyn-multiproject-artifacts.ps1` detects conflict artifacts independently of diagnostic ID, preflights directory repairs without partial writes, repairs only proven structural patterns, and refuses unrecognized shapes. Completion requires the same scoped formatter gate plus an artifact scan before affected builds and relevant tests. Technology-neutral work remains unaffected. Performs the requested work (not just a review), loads only relevant `references/`, respects repository conventions, scales process depth without lowering the standard, and reports evidence and risk honestly in concise feedback that may sacrifice grammar but never required evidence. Governing principle: consistency is key. | @@ -628,7 +628,7 @@ Test-project refactoring is deceptively lifecycle-sensitive. A `WebApplicationFa - **Real entry-point coverage** — focused and shared postconditions reject test-owned `WebApplication`/`TestServer` pipelines that can pass while the production `Program` is broken, - **Generic Host boundary** — non-web tests use `ApplicationTestFactory` or `ApplicationTest`; missing host seams are reported precisely unless production adaptation is authorized, - **Bootstrapper host fidelity** — Startup-based hosts and `MinimalConsoleProgram`, `MinimalWorkerProgram`, or `MinimalWebProgram` hosts remain in their established family instead of being rewritten for test convenience, -- **Dynamic compatibility** — stable package versions come from NuGet and must pass an isolated restore for the selected target frameworks, +- **Dynamic compatibility** — stable package versions come from NuGet and must pass isolated compatibility-project restores, including the selected combined package set and target frameworks, - **Source-grounded bootstrap** — new projects receive at least one behavior test derived from real source instead of a placeholder, - **Deterministic evidence** — inspection JSON reports roles, frameworks, xUnit generation, package owners, inheritance, migrations, recommendations, and blockers before mutation. From e417ac37005fa880cb125b88d53bf537211489d6 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 17:52:23 +0200 Subject: [PATCH 11/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20resolver?= =?UTF-8?q?=20to=20validate=20combined=20package=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve-test-package-versions.ps1 and validate-skill.ps1 refactored to support combined compatibility-project restores. The resolver now validates packages together as a set rather than individually, ensuring all selected packages restore successfully when combined. Validation script updated to include regression testing for the resolver. --- .../scripts/resolve-test-package-versions.ps1 | 73 +++++++++++++++---- skills/dotnet-test/scripts/validate-skill.ps1 | 5 +- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 b/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 index 804a4bc..1ca70de 100644 --- a/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 +++ b/skills/dotnet-test/scripts/resolve-test-package-versions.ps1 @@ -31,7 +31,7 @@ function Get-VersionKey { } function Test-PackageCompatibility { - param([string]$Id, [string]$Version, [string[]]$Frameworks, [string]$Workspace) + param([object[]]$Packages, [string[]]$Frameworks, [string]$Workspace) $projectPath = Join-Path $Workspace 'compatibility.csproj' $frameworkElement = if ($Frameworks.Count -eq 1) { @@ -39,6 +39,11 @@ function Test-PackageCompatibility { } else { "$($Frameworks -join ';')" } + $packageReferences = @( + foreach ($package in @($Packages)) { + ' ' -f $package.packageId, $package.version + } + ) -join [Environment]::NewLine $xml = @" @@ -46,7 +51,7 @@ function Test-PackageCompatibility { $(Join-Path $Workspace 'packages') - +$packageReferences "@ @@ -81,7 +86,7 @@ $workspace = Join-Path ([System.IO.Path]::GetTempPath()) ('dotnet-test-package-r New-Item -ItemType Directory -Path $workspace -Force | Out-Null try { - $resolved = [System.Collections.Generic.List[object]]::new() + $packageCandidates = [System.Collections.Generic.List[object]]::new() foreach ($id in @($packageIds | Sort-Object -Unique)) { $indexUrl = '{0}{1}/index.json' -f $packageBaseAddress, $id.ToLowerInvariant() try { $index = Invoke-RestMethod -Uri $indexUrl } catch { throw "NuGet lookup failed for '$id' at '$indexUrl': $($_.Exception.Message)" } @@ -91,23 +96,64 @@ try { Sort-Object major, minor, patch, revision -Descending | Select-Object -First $MaximumCandidates) if ($candidates.Count -eq 0) { throw "NuGet returned no stable versions for '$id'." } + $packageCandidates.Add([pscustomobject]@{ packageId = $id; source = $indexUrl; candidates = $candidates }) + } - $selected = $null + function Resolve-PackageSet { + param([int]$Index, [object[]]$Selected) + + if ($Index -ge $packageCandidates.Count) { + return [pscustomobject]@{ + compatible = $true + packages = @($Selected) + } + } + + $package = $packageCandidates[$Index] $lastFailure = $null - foreach ($candidate in $candidates) { + foreach ($candidate in @($package.candidates)) { $packageWorkspace = Join-Path $workspace ([System.IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Path $packageWorkspace -Force | Out-Null - $compatibility = Test-PackageCompatibility -Id $id -Version $candidate.text -Frameworks $TargetFramework -Workspace $packageWorkspace - if ($compatibility.compatible) { - $selected = $candidate.text - break + $selectedPackage = [pscustomobject]@{ + packageId = $package.packageId + version = $candidate.text + source = $package.source } - $lastFailure = $compatibility.output + $trial = @($Selected) + $selectedPackage + $compatibility = Test-PackageCompatibility -Packages $trial -Frameworks $TargetFramework -Workspace $packageWorkspace + if (-not $compatibility.compatible) { + $lastFailure = $compatibility.output + continue + } + + $resolution = Resolve-PackageSet -Index ($Index + 1) -Selected $trial + if ($resolution.compatible) { + return $resolution + } + $lastFailure = $resolution.output } - if ($null -eq $selected) { - throw "No stable '$id' version among the newest $($candidates.Count) candidates restored for '$($TargetFramework -join ';')'. Last restore output:`n$lastFailure" + + return [pscustomobject]@{ + compatible = $false + packageId = $package.packageId + candidateCount = @($package.candidates).Count + output = $lastFailure } - $resolved.Add([pscustomobject]@{ packageId = $id; version = $selected; source = $indexUrl; compatibility = 'isolated restore passed' }) + } + + $resolution = Resolve-PackageSet -Index 0 -Selected @() + if (-not $resolution.compatible) { + throw "No stable '$($resolution.packageId)' version among the newest $($resolution.candidateCount) candidates restored with a compatible package set for '$($TargetFramework -join ';')'. Last restore output:`n$($resolution.output)" + } + + $resolved = [System.Collections.Generic.List[object]]::new() + foreach ($package in @($resolution.packages)) { + $resolved.Add([pscustomobject]@{ + packageId = $package.packageId + version = $package.version + source = $package.source + compatibility = 'combined restore passed' + }) } [ordered]@{ @@ -118,4 +164,3 @@ try { } finally { if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force } } - diff --git a/skills/dotnet-test/scripts/validate-skill.ps1 b/skills/dotnet-test/scripts/validate-skill.ps1 index e4ad5e6..b706269 100644 --- a/skills/dotnet-test/scripts/validate-skill.ps1 +++ b/skills/dotnet-test/scripts/validate-skill.ps1 @@ -4,7 +4,7 @@ $skillRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $required = @( 'SKILL.md', 'FORMS.md', 'evals/evals.json', - 'scripts/inspect-dotnet-tests.ps1', 'scripts/resolve-test-package-versions.ps1', 'scripts/test-inspect-dotnet-tests.ps1', + 'scripts/inspect-dotnet-tests.ps1', 'scripts/resolve-test-package-versions.ps1', 'scripts/test-inspect-dotnet-tests.ps1', 'scripts/test-resolve-test-package-versions.ps1', 'references/unit-tests.md', 'references/web-functional-tests.md', 'references/application-functional-tests.md', 'references/bootstrapper-hosts.md', 'references/xunit-v3-modernization.md', 'references/migration-invariants.md', 'assets/unit/BehaviorTest.cs', 'assets/web/FocusedWebApplicationTest.cs', 'assets/web/SharedWebApplicationTest.cs', @@ -25,6 +25,9 @@ foreach ($needle in @('WebApplicationTestFactory', 'ApplicationTestFactory', 'Ma } if (-not $skill.Contains('An MTP executable run may supplement that gate but never replaces it', [System.StringComparison]::Ordinal)) { throw 'SKILL.md must reject MTP executable substitution for requested dotnet test validation.' } +& pwsh -NoProfile -File (Join-Path $PSScriptRoot 'test-resolve-test-package-versions.ps1') +if ($LASTEXITCODE -ne 0) { throw "Resolver regression failed with exit code $LASTEXITCODE." } + $evals = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'evals/evals.json')) foreach ($needle in @('WebApplicationTestFactory.Create', 'ManagedWebApplicationFixture', 'WebApplication.CreateBuilder', 'focused inspector postcondition')) { if (-not $evals.Contains($needle, [System.StringComparison]::Ordinal)) { throw "Focused-web eval is missing regression contract: $needle" } From 2382cc06fa72e2203767bb3a6609340de488bfb7 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 17:52:32 +0200 Subject: [PATCH 12/16] =?UTF-8?q?=E2=9C=85=20add=20resolver=20compatibilit?= =?UTF-8?q?y=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-resolve-test-package-versions.ps1 added to test the resolver's combined package compatibility validation logic, ensuring the resolver correctly identifies compatible package sets through isolated restores. --- .../test-resolve-test-package-versions.ps1 | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 skills/dotnet-test/scripts/test-resolve-test-package-versions.ps1 diff --git a/skills/dotnet-test/scripts/test-resolve-test-package-versions.ps1 b/skills/dotnet-test/scripts/test-resolve-test-package-versions.ps1 new file mode 100644 index 0000000..e69b011 --- /dev/null +++ b/skills/dotnet-test/scripts/test-resolve-test-package-versions.ps1 @@ -0,0 +1,218 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$resolver = Join-Path $PSScriptRoot 'resolve-test-package-versions.ps1' +$serviceIndexUri = 'https://api.nuget.org/v3/index.json' +$packageBaseAddress = 'https://mock.nuget/flatcontainer/' + +function Reset-ResolverMock { + $global:DotnetTestResolverVersions = @{} + $global:DotnetTestResolverRestoreRequests = [System.Collections.Generic.List[object]]::new() + $global:DotnetTestResolverFailureMode = 'Success' + $global:DotnetTestResolverFailurePackageId = $null + $global:DotnetTestResolverFailureVersion = $null + $global:DotnetTestResolverFailureCombination = $null + $global:LASTEXITCODE = 0 +} + +function Set-TestPackageVersions { + param( + [Parameter(Mandatory = $true)] + [string]$Id, + + [Parameter(Mandatory = $true)] + [string[]]$Versions + ) + + $global:DotnetTestResolverVersions[$Id.ToLowerInvariant()] = @($Versions) +} + +function Invoke-RestMethod { + param([Parameter(Mandatory = $true)][string]$Uri) + + if ($Uri -eq $serviceIndexUri) { + return [pscustomobject]@{ + resources = @([pscustomobject]@{ + '@type' = 'PackageBaseAddress/3.0.0' + '@id' = $packageBaseAddress + }) + } + } + + if ($Uri.StartsWith($packageBaseAddress, [System.StringComparison]::Ordinal)) { + $segments = $Uri.TrimEnd('/') -split '/' + $id = $segments[$segments.Count - 2] + if (-not $global:DotnetTestResolverVersions.ContainsKey($id)) { + throw "Unexpected package lookup: $Uri" + } + + return [pscustomobject]@{ versions = @($global:DotnetTestResolverVersions[$id]) } + } + + throw "Unexpected HTTP lookup: $Uri" +} + +function dotnet { + param( + [string]$Command, + [string]$ProjectPath, + [Parameter(ValueFromRemainingArguments = $true)] + [object[]]$Arguments + ) + + [xml]$project = [System.IO.File]::ReadAllText($ProjectPath) + $references = @($project.SelectNodes('/Project/ItemGroup/PackageReference') | ForEach-Object { + [pscustomobject]@{ + id = $_.GetAttribute('Include') + version = $_.GetAttribute('Version') + } + }) + $referenceKey = ($references | ForEach-Object { '{0}={1}' -f $_.id, $_.version } | Sort-Object) -join ';' + $global:DotnetTestResolverRestoreRequests.Add([pscustomobject]@{ + references = @($references) + key = $referenceKey + }) + + $failure = $null + if ($global:DotnetTestResolverFailureMode -eq 'FailAll') { + $failure = 'NU_TEST_RESTORE_FAILURE: forced restore failure.' + } elseif ($global:DotnetTestResolverFailureMode -eq 'FailPackageVersion' -and + @($references | Where-Object { $_.id -eq $global:DotnetTestResolverFailurePackageId -and $_.version -eq $global:DotnetTestResolverFailureVersion }).Count -gt 0) { + $failure = 'NU_TEST_RESTORE_FAILURE: forced candidate failure.' + } elseif ($global:DotnetTestResolverFailureMode -eq 'FailCombination' -and $referenceKey -eq $global:DotnetTestResolverFailureCombination) { + $failure = 'NU_TEST_RESTORE_FAILURE: forced combined-package failure.' + } + + if ($null -ne $failure) { + $global:LASTEXITCODE = 1 + Write-Output $failure + return + } + + $global:LASTEXITCODE = 0 + Write-Output 'restore succeeded' +} + +function Invoke-TestResolver { + param( + [Parameter(Mandatory = $true)] + [string[]]$PackageId, + + [Parameter(Mandatory = $true)] + [int]$MaximumCandidates + ) + + $output = @() + $caught = $false + try { + $output = @(& $resolver -TargetFramework net10.0 -Role Unit -PackageId $PackageId -MaximumCandidates $MaximumCandidates 2>&1) + } catch { + $caught = $true + $output += $_ + } + + $text = ($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine + $exitCode = [int]$global:LASTEXITCODE + $json = $null + if (-not $caught -and $exitCode -eq 0) { + try { + $json = $text | ConvertFrom-Json + } catch { + throw "Resolver returned invalid JSON: $text" + } + } + + return [pscustomobject]@{ + exitCode = $exitCode + text = $text + json = $json + } +} + +function Assert-Equal { + param( + [Parameter(Mandatory = $true)]$Actual, + [Parameter(Mandatory = $true)]$Expected, + [Parameter(Mandatory = $true)][string]$Because + ) + + if ("$Actual" -ne "$Expected") { + throw "Assertion failed: $Because. Expected '$Expected', got '$Actual'." + } +} + +function Assert-True { + param( + [Parameter(Mandatory = $true)][bool]$Condition, + [Parameter(Mandatory = $true)][string]$Because + ) + + if (-not $Condition) { throw "Assertion failed: $Because." } +} + +function Assert-ContainsText { + param( + [Parameter(Mandatory = $true)][string]$Text, + [Parameter(Mandatory = $true)][string]$Expected, + [Parameter(Mandatory = $true)][string]$Because + ) + + if (-not $Text.Contains($Expected, [System.StringComparison]::Ordinal)) { + throw "Assertion failed: $Because. Missing '$Expected'. Actual output:`n$Text" + } +} + +try { + Reset-ResolverMock + + Set-TestPackageVersions -Id 'Stable.Package' -Versions @('11.0.0-rc.1', '10.0.0', '9.99.0', '9.98.0') + $stable = Invoke-TestResolver -PackageId 'Stable.Package' -MaximumCandidates 2 + Assert-Equal -Actual $stable.exitCode -Expected 0 -Because 'stable candidate resolution should succeed' + Assert-Equal -Actual $stable.json.packages[0].version -Expected '10.0.0' -Because 'stable versions must be numerically ordered and prereleases excluded' + + Reset-ResolverMock + Set-TestPackageVersions -Id 'Fallback.Package' -Versions @('3.0.0', '2.0.0') + $global:DotnetTestResolverFailureMode = 'FailPackageVersion' + $global:DotnetTestResolverFailurePackageId = 'Fallback.Package' + $global:DotnetTestResolverFailureVersion = '3.0.0' + $fallback = Invoke-TestResolver -PackageId 'Fallback.Package' -MaximumCandidates 2 + Assert-Equal -Actual $fallback.exitCode -Expected 0 -Because 'an older restorable candidate should be selected' + Assert-Equal -Actual $fallback.json.packages[0].version -Expected '2.0.0' -Because 'candidate fallback should continue after a restore failure' + Assert-Equal -Actual $global:DotnetTestResolverRestoreRequests.Count -Expected 2 -Because 'the failed newest candidate and fallback candidate should both be tested' + + Reset-ResolverMock + Set-TestPackageVersions -Id 'Package.A' -Versions @('2.0.0', '1.0.0') + Set-TestPackageVersions -Id 'Package.B' -Versions @('2.0.0', '1.0.0') + $global:DotnetTestResolverFailureMode = 'FailCombination' + $global:DotnetTestResolverFailureCombination = 'Package.A=2.0.0;Package.B=2.0.0' + $combined = Invoke-TestResolver -PackageId @('Package.A', 'Package.B') -MaximumCandidates 2 + Assert-Equal -Actual $combined.exitCode -Expected 0 -Because 'the resolver should recover from an incompatible combined package set' + Assert-Equal -Actual (($combined.json.packages | Where-Object packageId -eq 'Package.A').version) -Expected '2.0.0' -Because 'the compatible first package candidate should be retained' + Assert-Equal -Actual (($combined.json.packages | Where-Object packageId -eq 'Package.B').version) -Expected '1.0.0' -Because 'the incompatible newest second package candidate should fall back' + Assert-True -Condition (@($global:DotnetTestResolverRestoreRequests | Where-Object { $_.references.Count -eq 2 }).Count -gt 0) -Because 'compatibility must be tested with the combined package set' + Assert-Equal -Actual ($combined.json.packages | Where-Object packageId -eq 'Package.A').compatibility -Expected 'combined restore passed' -Because 'the output must describe the compatibility actually validated' + + Reset-ResolverMock + Set-TestPackageVersions -Id 'Failure.Package' -Versions @('2.0.0', '1.0.0') + $global:DotnetTestResolverFailureMode = 'FailAll' + $failure = Invoke-TestResolver -PackageId 'Failure.Package' -MaximumCandidates 2 + Assert-True -Condition ($failure.exitCode -ne 0) -Because 'resolver restore failure must fail closed' + Assert-ContainsText -Text $failure.text -Expected "No stable 'Failure.Package' version" -Because 'failure output must identify the package and candidate scope' + Assert-ContainsText -Text $failure.text -Expected 'NU_TEST_RESTORE_FAILURE' -Because 'failure output must preserve restore evidence' + + Write-Output 'resolve-test-package-versions.ps1 regression: PASS' +} finally { + foreach ($name in @( + 'DotnetTestResolverVersions', + 'DotnetTestResolverRestoreRequests', + 'DotnetTestResolverFailureMode', + 'DotnetTestResolverFailurePackageId', + 'DotnetTestResolverFailureVersion', + 'DotnetTestResolverFailureCombination' + )) { + Remove-Variable -Scope Global -Name $name -ErrorAction SilentlyContinue + } +} From 4c10c651f5ac7c99106cfb2ff4b5d81a9a6a0898 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 17:52:41 +0200 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=94=A8=20update=20repo=20validation?= =?UTF-8?q?=20tooling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/validate-skill-templates.ps1 updated to support validation of the enhanced dotnet-test resolver behavior and new test file requirements. --- scripts/validate-skill-templates.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index 0f328cb..8939eb4 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -1119,6 +1119,7 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific $migration = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/references/migration-invariants.md' -GitRef $Ref $inspect = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/scripts/inspect-dotnet-tests.ps1' -GitRef $Ref $resolve = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/scripts/resolve-test-package-versions.ps1' -GitRef $Ref + $resolveTest = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/scripts/test-resolve-test-package-versions.ps1' -GitRef $Ref $evals = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/evals/evals.json' -GitRef $Ref $fixtureFiles = Get-RepoFileList -RepoRoot $repoRoot -RelativePath 'skills/dotnet-test/evals/files' -GitRef $Ref @@ -1155,7 +1156,11 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'hostTestOwnerships' Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle 'packageOwnership' Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'https://api.nuget.org/v3/index.json' - Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'isolated restore passed' + Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'Test-PackageCompatibility -Packages $trial' + Assert-Contains -Name 'resolve-test-package-versions.ps1' -Content $resolve -Needle 'combined restore passed' + Assert-Contains -Name 'test-resolve-test-package-versions.ps1' -Content $resolveTest -Needle 'stable candidate resolution should succeed' + Assert-Contains -Name 'test-resolve-test-package-versions.ps1' -Content $resolveTest -Needle 'combined package set' + Assert-Contains -Name 'test-resolve-test-package-versions.ps1' -Content $resolveTest -Needle 'restore evidence' $evalObject = $evals | ConvertFrom-Json if (@($evalObject.evals).Count -ne 6) { From 1806060f21de61b2dcdbde20373ad02af744b018 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 20:28:02 +0200 Subject: [PATCH 14/16] =?UTF-8?q?=F0=9F=92=AC=20finalize=20v0.9.0=20releas?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finalize v0.9.0 release notes documenting the dotnet-test skill introduction with comprehensive xUnit migration, test-role classification, managed-fixture patterns, and validation tooling. Update README.md skill inventory, installation snippet, and capability descriptions reflecting new dotnet-test capability and enhanced release-entity classification in git-keep-a-changelog and git-nuget-release-notes. --- CHANGELOG.md | 36 +++++++++++++++++++++++------------- README.md | 3 ++- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e023a9..6dae437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,21 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [0.9.0] - 2026-08-09 -This is a minor release introducing the `dotnet-test` skill, a comprehensive xUnit migration and bootstrapping tool for Codebelt conventions. The release classifies test roles (ordinary unit, ASP.NET Core functional, console/worker functional), preserves host configuration and lifecycle semantics, eliminates legacy `WebApplicationFactory` patterns in favor of role-specific managed fixtures, modernizes xUnit v2 to v3/Microsoft Testing Platform, and includes deterministic project inspection, package-version resolution, eval coverage, and repository validation tooling. +This is a minor release introducing the `dotnet-test` skill for lifecycle-aware xUnit test migration and refactoring across ordinary unit tests, ASP.NET Core functional tests, and console/worker service tests. The release includes comprehensive test-role classification, managed-fixture patterns preserving composition-root control, xUnit v3 modernization guidance, deterministic package-version resolution, and integration-test bootstrapper hosts. Enhanced release-entity classification in `git-keep-a-changelog` and `git-nuget-release-notes` now distinguishes new-capability introductions from pre-existing refinements, preventing mis-categorized changelog entries when unreleased features are refined before first release. Repository validation tooling is strengthened with skill-content validation and resolver script enforcement. ### Added -- `dotnet-test` skill for classifying, bootstrapping, and refactoring xUnit projects across ordinary unit, ASP.NET Core functional, and console/worker functional test roles with refined WebApplicationTestFactory bootstrap pattern documentation ensuring Program composition root preservation without pipeline reconstruction, -- Deterministic .NET test project inspection via `inspect-dotnet-tests.ps1` extracting target frameworks, test project markers, Generic Host resolution, `WebApplicationFactory` usages, expected application patterns, host test ownerships, and package ownership with additional property discovery for role-specific inspector routing, -- `resolve-test-package-versions.ps1` NuGet-backed package-version resolver for compatible xUnit, Microsoft Testing Platform, and supporting packages across stable isolated restores, -- Structured FORMS.md parameter collection for test role selection, operation mode (fresh bootstrap vs. migration), and host ownership determination, -- Six comprehensive eval scenarios covering fresh unit tests, xUnit v2 modernization, focused web functional tests with `WebApplicationTestFactory`, shared web functional tests with managed fixtures, shared non-web functional tests, and worker-service functional tests, with paired `with_skill` and `without_skill` comparison runs and associated fixture projects, with enhanced prompts and postconditions emphasizing focused-inspector bootstrap contract and managed-fixture presence validation, -- Role-specific reference documentation (`unit-tests.md`, `web-functional-tests.md`, `application-functional-tests.md`, `bootstrapper-hosts.md`, `migration-invariants.md`, `xunit-v3-modernization.md`) covering test patterns, fixture lifecycle, Generic Host boundaries, and migration guidance with clarified lifecycle semantics, managed-fixture deprecation timeline, and entrypoint-owned composition-root preservation, -- Codebelt xUnit and bootstrapper asset templates covering focused/shared unit tests, focused/shared web-application tests, ordinary console/worker functional tests, and bootstrapper Program/Startup patterns for `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts, -- Entrypoint-owned managed fixture patterns (`ManagedWebApplicationFixture`, `ManagedApplicationFixture`) as permanent replacements for deprecated blocking-fixture patterns, -- Lifecycle-preserving functional test migration that retains configuration, lazy startup, synchronous/asynchronous disposal, service access, and test isolation while eliminating `WebApplicationFactory` reconstruction patterns, -- Repository validation rules in `scripts/validate-skill-templates.ps1` for dotnet-test coverage, role-specific encoding, managed-fixture presence, deprecated blocking-fixture rejection, bootstrapper host fidelity, and eval scenario count and content verification; updated to expect six paired eval scenarios and validate managed-fixture references instead of deprecated blocking-fixture patterns, -- Enhanced README.md skill inventory, installation snippet, and "Why dotnet-test?" motivational section explaining lifecycle sensitivity, test-role classification, entrypoint-owned managed fixture, synchronous/asynchronous disposal, and host-family boundary preservation. +- `dotnet-test` skill providing lifecycle-aware xUnit test migration and modernization guidance with role-specific patterns for ordinary unit tests, ASP.NET Core WebApplicationFactory elimination, and console/worker service functional-test bootstrapping, +- Comprehensive test-role classification in `dotnet-test` covering focused vs. shared fixtures, managed-fixture entrypoint composition, Generic Host seam preservation, and WebApplicationFactory elimination patterns without pipeline reconstruction, +- `dotnet-test` SKILL.md with step-by-step test-project inspection, xUnit v3 modernization paths, managed-fixture bootstrap hosts, role-specific reference-document guidance, structured parameter collection via FORMS.md, and test-package compatibility validation, +- Role-specific test assets in `dotnet-test/assets/` covering unit-test behavior patterns, focused and shared web-application fixtures, application-focused fixtures, and bootstrapper hosts for console and worker services in both minimal and traditional Program/Startup configurations, +- Comprehensive `dotnet-test` eval scenarios with paired test cases covering fresh xUnit unit-test projects, ASP.NET Core focused functional tests with managed fixtures, shared web-application functional tests, xUnit v2-to-v3 modernization, and worker service functional tests with GenericHost seams, +- `dotnet-test` reference documentation covering unit-test fundamentals, web-functional-test patterns, application-functional-test fixtures, bootstrapper-host programs for console and worker services, xUnit v3 modernization guidance, and migration-invariant preservation rules, +- `dotnet-test` package-compatibility resolver script `resolve-test-package-versions.ps1` validating combined package restore across selected NuGet candidates for multiple target frameworks, preventing incompatible package combinations in managed-fixture test projects, +- Test coverage for `dotnet-test` package-compatibility resolver via `test-resolve-test-package-versions.ps1` validating resolver behavior, compatibility detection, and framework coverage, +- `resolve-release-entity.ps1` script for `git-keep-a-changelog` enabling deterministic classification of base-to-HEAD change outcomes (`Added`, `Removed`, `Changed`, or `Unchanged`) at release boundaries, supporting per-entity classification separate from intermediate commit verbs, +- Test coverage for `git-keep-a-changelog` release-entity classification via `test-resolve-release-entity.ps1` validating classification outcomes and boundary handling. + +### Changed + +- Enhanced `git-keep-a-changelog` SKILL.md with improved release-entity classification guidance using the new `resolve-release-entity.ps1` helper for deterministic base-state analysis, eliminating mis-categorization of pre-existing capability refinements as `Changed` or `Fixed` when they should remain under `Added` for new capabilities, +- Updated `git-keep-a-changelog` Step 4e guidance to run the bundled release-entity classifier for path-backed entities, treating its emitted classification as authoritative and avoiding commit-verb-based category inference, +- Enhanced `git-keep-a-changelog` deterministic reduction model with improved reconciliation rules and examples showing how surviving-outcome classification prevents duplicate changelog entries when unreleased drafts are refined with multiple commits before first release, +- Improved `git-keep-a-changelog` bad-output-characteristics section with explicit warnings about placing pre-release refinements under `Changed` or `Fixed` instead of preserving them under the initial `Added` outcome, +- Enhanced `git-nuget-release-notes` SKILL.md with improved release-entity classification guidance aligned with `git-keep-a-changelog` enhancements, including per-package classification and cumulative-package-set reduction patterns, +- Updated repository validation to enforce `resolve-release-entity.ps1` presence in git-keep-a-changelog and validate adoption of entity-classification patterns in release-notes skills, +- README.md skill inventory and descriptions updated to reflect `dotnet-test` capability, enhanced release-entity classification in `git-keep-a-changelog` and `git-nuget-release-notes`, and improved validation tooling, +- Enhanced `scripts/validate-skill-templates.ps1` with deterministic skill-content validation, release-entity classifier enforcement, git-keep-a-changelog trigger validation, and resolver-script presence checks. ## [0.8.2] - 2026-08-07 @@ -569,7 +579,7 @@ This is a minor release that introduces two complementary git workflow skills, e - Improved scaffold fidelity with hidden `.bot` asset preservation, explicit UTF-8 and BOM handling, and checks aimed at preventing mojibake or incomplete generated output. -[Unreleased]: https://github.com/codebeltnet/agentic/compare/v0.9.0...HEAD +[Unreleased]: https://github.com/codebeltnet/agentic/compare/v0.8.2...HEAD [0.9.0]: https://github.com/codebeltnet/agentic/compare/v0.8.2...v0.9.0 [0.8.2]: https://github.com/codebeltnet/agentic/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/codebeltnet/agentic/compare/v0.8.0...v0.8.1 diff --git a/README.md b/README.md index 28efed9..8db5a38 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ npx skills add https://github.com/codebeltnet/agentic --skill agent-smith | Skill | Description | |-------|-------------| | [git-visual-commits](skills/git-visual-commits/SKILL.md) | AI-driven git commit workflow with deterministically validated emoji-first subjects (gitmoji-first), optional conventional prefixes only on explicit request, and three identity modes: bot-attributed (`git bot commit`), human-attributed (`git commit`), and collaborative (`git our commit` — agent analyzes authorship, human picks attribution). Its first critical rule requires a complete SKILL.md read through EOF, then a bundled PowerShell gate rejects unapproved emoji, anything other than one separator space, uppercase description beginnings, and subjects over 70 characters before plan display and immediately before Git. Multi-file plans that initially collapse to one category also require a visible full-context quality gate; one-file changes keep the fast path. Includes commit body by default (opt out with `no-body`), semantic intent splitting, clarification-before-correction safety, and auto-approval mode (`yolo` / `auto`) that cannot bypass validation. Stack-agnostic. | -| [git-keep-a-changelog](skills/git-keep-a-changelog/SKILL.md) | Git-aware Keep a Changelog companion that creates or updates `CHANGELOG.md` from the current branch by default. Bundled deterministic resolvers separate branch-unique commit history from merge-base-to-`HEAD` net diffs, exclude the previous-release or comparison boundary, fail on base-history bleed, and classify explicit path-backed release entities as `Added`, `Removed`, `Changed`, or `Unchanged` from their base and final-state existence. The skill reduces the selected range to surviving outcomes before section classification and establishes each user-facing release entity against the base, so a new skill plus its pre-release refinements, documentation, validators, and eval wiring remains one `Added` outcome. It inspects dependency and version manifests before commit bodies, treats the selected branch or explicit range as author-agnostic so all PR contributors remain included, infers a release heading from a branch version hint like `v0.3.0/...`, asks a mandatory `Yes / No / Custom` question before including pending worktree changes in ordinary concrete-release drafts, and keeps yolo/auto limited to automatic staged, unstaged, and untracked inclusion without widening committed history. It also creates missing changelogs, writes required SemVer-aware highlights, maintains compare-link footers, preserves natural prose wrapping, and curates standard Keep a Changelog sections instead of dumping raw commit logs. | +| [git-keep-a-changelog](skills/git-keep-a-changelog/SKILL.md) | Git-aware Keep a Changelog companion that creates or updates `CHANGELOG.md` from the current branch by default. Bundled deterministic resolvers separate branch-unique commit history from merge-base-to-`HEAD` net diffs, exclude the previous-release or comparison boundary, fail on base-history bleed, and classify explicit path-backed release entities as `Added`, `Removed`, `Changed`, or `Unchanged` from their base and final-state existence. The skill reduces the selected range to surviving outcomes before section classification and establishes each user-facing release entity against the base, so a new skill plus its pre-release refinements, documentation, validators, and eval wiring remains one `Added` outcome. When a concrete version heading already exists but the matching tag is still absent, it rewrites that draft from the current base-to-`HEAD` truth instead of treating the older draft text as a second baseline. It inspects dependency and version manifests before commit bodies, treats the selected branch or explicit range as author-agnostic so all PR contributors remain included, infers a release heading from a branch version hint like `v0.3.0/...`, asks a mandatory `Yes / No / Custom` question before including pending worktree changes in ordinary concrete-release drafts, and keeps yolo/auto limited to automatic staged, unstaged, and untracked inclusion without widening committed history. It also creates missing changelogs, writes required SemVer-aware highlights, maintains compare-link footers, preserves natural prose wrapping, and curates standard Keep a Changelog sections instead of dumping raw commit logs. | | [git-nuget-release-notes](skills/git-nuget-release-notes/SKILL.md) | Git-aware NuGet release-notes companion for .NET repos that keep cumulative `.nuget/{ProjectName}/PackageReleaseNotes.txt` files. Discovers packable `src/` projects, resolves concrete package version and availability, creates missing files when needed, reduces each package to its surviving base-to-`HEAD` delta before classifying history, and establishes each package capability against the base so pre-release refinements and fixes to a new capability remain one `ADDED` New Feature. It writes per-package `ALM` / `Breaking Changes` / `New Features` / `Improvements` / `Bug Fixes` style notes from final package state plus supporting commit context instead of dumping commit subjects. | | [git-nuget-readme](skills/git-nuget-readme/SKILL.md) | Git-aware NuGet README companion for .NET repos that advertise a package from `src/`. Resolves the real packable project the README should sell, combines git history with actual package metadata, source capabilities, and relevant tests when feasible, preserves honest badge/docs/contributing sections, and writes a forthcoming, adoption-friendly `README.md` with repo-derived branding, clear value, install, framework-support, and quick-start guidance. | | [git-visual-squash-summary](skills/git-visual-squash-summary/SKILL.md) | Non-mutating grouped-summary companion to `git-visual-commits`. Turns the full current feature branch into a curated set of compact lowercase-start summary lines for PR or squash-and-merge contexts by default, comparing against the repository base branch rather than a same-named tracking remote, including commits from all authors unless explicitly narrowed, reducing the cumulative base-to-`HEAD` delta first so reverted churn disappears, preserving technical identifiers, merging overlap, keeping surviving dependency/version changes separate from build/refactor work when the final diff still shows them, and avoiding changelog-style wording, unsupported claims, yolo prompts, needless commit-range questions, or commit-selection UI for ordinary branch-level squash requests. | @@ -307,6 +307,7 @@ Writing `CHANGELOG.md` well is harder than it looks. Raw commit subjects are too - **Keep a Changelog first** — writes `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security` sections in the expected style - **Full-commit context** — reads complete commit messages and the net diff before writing - **History is evidence, result is truth** — reduces the selected range to surviving base-to-`HEAD` outcomes before section classification, so reverted work disappears and one surviving capability is described once +- **Unreleased draft rewrites** — if a version-branch heading already exists but its tag does not, the skill regenerates that draft from git truth so new-capability refinements stay under `Added` until release - **Deterministic release isolation** — resolves the real comparison branch, excludes its merge boundary, and verifies that no commit already on the base branch can bleed into the new release - **PR-complete history** — keeps every branch-unique commit from every contributor while avoiding a same-name feature tracking ref as the comparison base - **Cumulative dependency coverage** — when version manifests changed across the release range, diffs them from base to `HEAD` so the changelog reflects the surviving package/version story instead of only per-commit fragments From 0f989ed8d05bf0b63d6576913fb5f3c962fc2e24 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 20:28:07 +0200 Subject: [PATCH 15/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20enhance=20git-keep-a?= =?UTF-8?q?-changelog=20with=20release-entity=20classification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance git-keep-a-changelog SKILL.md with improved release-entity classification guidance using the new resolve-release-entity.ps1 helper for deterministic base-state analysis. Add new eval scenarios validating classification outcomes and boundary handling. This prevents mis-categorization of pre-existing capability refinements as Changed or Fixed when they should remain under Added for new capabilities. --- skills/git-keep-a-changelog/SKILL.md | 10 ++++++++++ skills/git-keep-a-changelog/evals/evals.json | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/skills/git-keep-a-changelog/SKILL.md b/skills/git-keep-a-changelog/SKILL.md index e6978d1..b1e7ae0 100644 --- a/skills/git-keep-a-changelog/SKILL.md +++ b/skills/git-keep-a-changelog/SKILL.md @@ -39,6 +39,7 @@ When the user's request contains `yolo` or `auto` (case-insensitive, anywhere in - Never change range inclusivity because the changelog target is a concrete version instead of `[Unreleased]`. - Include commits from every author/contributor in the selected scope. Do not filter to the current git user, current contributor, bot identity, configured author, or "my changes" unless the user explicitly asks for an author-filtered changelog. - If the current branch starts with a version hint such as `v0.3.0/`, use that to target a concrete release heading. +- If a concrete target heading already exists but its matching `vX.Y.Z` tag does not, treat that heading as an unreleased draft and regenerate it from the resolved git result instead of preserving stale bullets as a second baseline. - Otherwise, target `## [Unreleased]`. - Always write a release highlight immediately below the target heading. - The release highlight must explicitly classify the release as `major`, `minor`, or `patch`. @@ -50,6 +51,7 @@ When the user's request contains `yolo` or `auto` (case-insensitive, anywhere in - If pending worktree changes exist for a concrete release draft, do not silently include or exclude them. Ask the user first with a short `Yes / No / Custom` prompt. **Exception: in yolo/auto mode, include all pending changes automatically without asking.** - Yolo/auto changes pending-worktree handling only. It never widens committed history or includes the comparison boundary. - Do not dump commit subjects verbatim into the changelog. +- Do not treat the current contents of the target heading as a release-classification baseline; git state is the baseline. - Do not invent unsupported changes, risks, or migration guidance. ## Mandatory Checkpoints @@ -72,6 +74,8 @@ History = provenance used to explain Result History is evidence; the resulting state is truth. +The current contents of the target heading are cached output, not a release baseline. When rerunning on an unreleased version branch whose matching tag is absent, discard the prior draft narrative and regenerate the heading from the resolved base-to-`HEAD` result so later refinements to the same new capability remain part of its `Added` outcome. + Reduce first. Interpret second. Summarize last. Establish the classification baseline at the user-facing release-entity boundary, not independently for every changed file. For a repo-managed skill, the entity is the skill capability together with its dedicated files and inseparable registration, catalog, documentation, validation, and eval wiring. If that entity is absent at the base and present at `HEAD`, its introduction is `Added`; intermediate commits that refine, fix, document, or validate it cannot create `Changed` or `Fixed` outcomes for that same new entity. A change to a separately pre-existing shared capability remains its own outcome and is classified from its own base state. @@ -102,6 +106,7 @@ Examples: - File deleted and recreated identically -> no changelog entry. - One capability added, revised, and still present -> usually one `Added` bullet describing its final form, not separate `Added`, `Changed`, and `Fixed` bullets. - New `skills/dotnet-test/` capability added, then documented, validated, and refined before release -> `Added` only for the complete shipped capability. README registration and validator/eval wiring whose sole purpose is that introduction stay part of the added outcome. +- Existing `## [0.9.0]` draft with one `Added` bullet, then more commits refine the same unreleased `dotnet-test` capability -> rewrite the draft so `dotnet-test` stays under `Added`; do not append `Changed` or `Fixed` just because the earlier draft already exists. ## Release Highlight Contract @@ -194,6 +199,7 @@ Determine whether to write a concrete release section or update `[Unreleased]`. When the user asks to "finalize", "ready to release", "rtr", "release", "publish", or "ship" (or similar release-intent words): - Extract the version from the current branch name if it starts with a version prefix such as `v0.3.0/feature-name`. +- When the target is `## [X.Y.Z]`, check whether `refs/tags/vX.Y.Z` exists locally. If it does not, any existing `## [X.Y.Z]` section is still a branch draft rather than released history. - Target `## [X.Y.Z] - YYYY-MM-DD` (today's date) for that extracted version. - This is a strong signal that the user wants to finalize that specific release in the changelog. @@ -202,6 +208,7 @@ Otherwise: - Strip the leading `v` from the visible changelog heading, but keep tag comparisons in `vX.Y.Z` form. - If no version hint exists, target `## [Unreleased]`. - If the target heading already exists, update it in place instead of duplicating it. +- For an existing concrete heading whose matching tag is absent, replace the release highlight and populated sections wholesale from the current resolved git evidence. Do not preserve an older `Added` bullet and then layer later pre-release refinements into `Changed` or `Fixed`. ### Step 3: Confirm Pending Worktree Changes (MANDATORY GATE) @@ -352,6 +359,7 @@ Preserve the file's existing structure while editing. - Keep the introduction and existing release history intact. - If writing a concrete release section, insert it below `## [Unreleased]` and above older releases. - If writing to `## [Unreleased]`, keep the heading and update only its content. +- When updating an existing target heading, rebuild the release highlight and populated sections from the newly resolved surviving outcomes. Delete or rewrite stale bullets that no longer reflect the final release story instead of incrementally patching around them. - On every edit, verify that the compare-link footer exists at the bottom of the file. If it is missing or incomplete, insert or repair it instead of leaving the changelog without diff ranges. - When adding or updating a concrete version, `[Unreleased]` should compare from the newest released version to `HEAD`, and that released version should compare from the previous version tag to the new tag. - Preserve valid historical compare links for older releases. Repair only the links that are missing, incomplete, or wrong. @@ -374,6 +382,7 @@ After updating `CHANGELOG.md`, stop and let the user review the file. Do not com - Treats the selected branch or range as author-agnostic scope and includes every contributor's commits unless the user explicitly narrows by author. - Treats Step 3 as a mandatory confirmation gate for concrete releases and asks the `Yes / No / Custom` question before including pending worktree changes (or skips Step 3 entirely and includes all changes when yolo/auto mode is active). - Keeps yolo/auto limited to pending-worktree inclusion and never uses autonomy mode to widen committed history. +- If an unreleased concrete version draft already exists, rewrites that draft from the current git truth so pre-release refinements to a base-absent capability remain under `Added`. - Maintains or inserts the compare-link footer at the bottom of the file on both create and update paths. - Preserves natural prose wrapping with no fixed column-width target. - Keeps bullets specific, concrete, non-repetitive, and consistently punctuated. @@ -386,6 +395,7 @@ After updating `CHANGELOG.md`, stop and let the user review the file. Do not com - Reporting temporary features, files, APIs, or dependencies that leave no surviving base-to-`HEAD` change. - Putting one surviving capability under multiple sections because its intermediate commits used different verbs. - Putting any part of a base-absent capability under `Changed` or `Fixed` because later commits refined, documented, validated, or fixed it before its first release. +- Using an older unreleased draft heading as a second baseline, preserving its earlier `Added` bullet and then appending `Changed` / `Fixed` for later commits to the same still-unreleased capability. - Omitting the release highlight. - Failing to classify the release as major, minor, or patch. - Refusing to proceed just because `CHANGELOG.md` does not exist yet. diff --git a/skills/git-keep-a-changelog/evals/evals.json b/skills/git-keep-a-changelog/evals/evals.json index da4e933..1f7ebd9 100644 --- a/skills/git-keep-a-changelog/evals/evals.json +++ b/skills/git-keep-a-changelog/evals/evals.json @@ -228,6 +228,18 @@ "Does not create a Fixed section or Fixed bullet for fixes made within the unreleased dotnet-test introduction cycle", "Would classify an independently changed pre-existing shared capability separately rather than hiding it inside the new skill outcome" ] + }, + { + "id": 20, + "prompt": "Create a deterministic temp git repo outside the current repository under `$env:TEMP`, then use git-keep-a-changelog there twice. Start from a tagged base release containing `CHANGELOG.md`, `README.md`, and `scripts/validate-skills.ps1`, with no `skills/dotnet-test/` directory and no dotnet-test registration. On branch `v0.9.0/dotnet-test`, commit these steps in order: introduce `skills/dotnet-test/SKILL.md`; add its references, assets, scripts, and evals; register it in README.md and the shared validator. Run the skill once so `CHANGELOG.md` contains a draft `## [0.9.0]` section with an `Added` bullet for dotnet-test. Then make these more unreleased commits on the same branch: refine `resolve-test-package-versions.ps1` to validate packages as a combined set, add `test-resolve-test-package-versions.ps1`, update `validate-skill.ps1` to run that regression test, and revise the README.md dotnet-test capability description. Re-run the skill to update `CHANGELOG.md` and stop after the edit.", + "expected_output": "The existing 0.9.0 draft section is regenerated from the branch's current base-to-HEAD result, keeping dotnet-test as one Added outcome in its final form instead of appending a Changed section for later pre-release refinements.", + "expectations": [ + "Treats the existing `## [0.9.0]` section as an unreleased draft because the matching `v0.9.0` tag does not exist yet", + "Regenerates the 0.9.0 highlight and populated sections from the resolved git result instead of using the earlier draft text as a second baseline", + "Keeps the combined-set package validation, resolver test, validator update, and README wording changes within the Added dotnet-test outcome because the capability is still base-absent", + "Does not preserve the earlier draft bullet as a frozen baseline that forces later refinements into `Changed`", + "Does not create a Changed section or Changed bullet for unreleased refinements to the still-unreleased dotnet-test introduction" + ] } ] } From 8f133275cb5e9d914aa4c00041cd9eda0a462fb9 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Sun, 9 Aug 2026 20:28:13 +0200 Subject: [PATCH 16/16] =?UTF-8?q?=F0=9F=94=A8=20add=20deterministic=20skil?= =?UTF-8?q?l=20validation=20to=20repository=20tooling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance scripts/validate-skill-templates.ps1 with deterministic skill-content validation, release-entity classifier enforcement, git-keep-a-changelog trigger validation, and resolver-script presence checks. Repository validation now requires resolve-release-entity.ps1 presence in git-keep-a-changelog and enforces adoption of entity-classification patterns in release-notes skills. --- scripts/validate-skill-templates.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index 8939eb4..b3b647e 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -1573,11 +1573,13 @@ Add-ValidationResult -Results $results -Name 'Git summary skills reduce ranges t Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Base absent and `HEAD` absent -> omit it.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Classify each user-facing release entity from whether it existed at the resolved base' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'intermediate commits that refine, fix, document, or validate it cannot create `Changed` or `Fixed` outcomes' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'The current contents of the target heading are cached output, not a release baseline.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Do not summarize commits one by one and deduplicate the prose afterward.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $changelogSkill -Needle 'Use history only to explain the surviving outcomes' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Omits `Foo` because it leaves no surviving base-to-HEAD change' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Does not add a Security or other section entry when the final diff contradicts the commit message claim' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Does not create a Changed section or Changed bullet for dotnet-test refinements made before its first release' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $changelogEvals -Needle 'Does not preserve the earlier draft bullet as a frozen baseline that forces later refinements into `Changed`' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'History is evidence; the resulting state is truth.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Classify each user-facing package capability from whether it existed at the resolved base'