Add eval tests for csharp-refactoring and dotnet-breaking-changes skills

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Abhitej John
2026-07-07 14:27:27 -07:00
parent d6a07d5b6d
commit 7900815c85
14 changed files with 630 additions and 230 deletions
+155 -230
View File
@@ -1,255 +1,180 @@
name: csharp-refactoring
description: Evaluates the dotnet/csharp-refactoring skill
type: capability
defaults:
timeout: 15m
stimuli:
- name: Rename a shipped public method without breaking existing callers
prompt: "In the Billing class library, rename the shipped public method `AppSettingsHelper.ParseIntSetting` to `ParseIntegerSetting` and migrate this solution's callers, while preserving compatibility for existing callers of the old public API. `PublicAPI.Shipped.txt` records the shipped surface. The solution is at Fixture.sln."
environment: &fixture-environment
files:
- src: Fixture.sln
dest: Fixture.sln
- src: src/Billing/AppSettingsHelper.cs
dest: src/Billing/AppSettingsHelper.cs
- src: src/Billing/Billing.csproj
dest: src/Billing/Billing.csproj
- src: src/Billing/Coupons.cs
dest: src/Billing/Coupons.cs
- src: src/Billing/Coupons.g.cs.template
dest: src/Billing/Coupons.g.cs.template
- src: src/Billing/OrderProcessor.cs
dest: src/Billing/OrderProcessor.cs
- src: src/Billing/PlatformInfo.cs
dest: src/Billing/PlatformInfo.cs
- src: src/Billing/Pricing.cs
dest: src/Billing/Pricing.cs
- src: src/Billing/PublicAPI.Shipped.txt
dest: src/Billing/PublicAPI.Shipped.txt
- src: tests/Billing.Tests/Billing.Tests.csproj
dest: tests/Billing.Tests/Billing.Tests.csproj
- src: tests/Billing.Tests/BillingTests.cs
dest: tests/Billing.Tests/BillingTests.cs
graders:
- type: file-contains
config:
path: src/Billing/AppSettingsHelper.cs
value: ParseIntegerSetting
- type: file-contains
config:
path: tests/Billing.Tests/BillingTests.cs
value: ParseIntegerSetting
- type: file-contains
config:
path: src/Billing/AppSettingsHelper.cs
value: Obsolete
- type: file-contains
config:
path: src/Billing/AppSettingsHelper.cs
value: ParseIntSetting
- type: run-command
config:
command: dotnet test Fixture.sln --verbosity normal
expected_exit_code: 0
stdout_contains: "Total tests: 10"
timeout: 5m
- type: output-matches
config:
pattern: (public API|compatib|obsolete|shim)
- type: output-matches
config:
pattern: (dotnet (build|test)|rebuild|re-?run[\s\S]{0,30}tests|tests?[\s\S]{0,30}(pass|green))
- type: prompt
scenarios:
- name: "Rename a method across its declaration and every caller"
prompt: "In the Billing class library, the method `OrderProcessor.DoStuff` is badly named for what it does — it computes an invoice. Rename it to `ComputeInvoice` everywhere it is declared and called, without changing any behavior. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
- type: "file_contains"
path: "src/Billing/OrderProcessor.cs"
value: "ComputeInvoice"
# The rename must propagate to every caller. BillingTests.cs calls the method
# directly, so a correct rename lands the new name here too (and the old call
# would otherwise fail to compile).
- type: "file_contains"
path: "tests/Billing.Tests/BillingTests.cs"
value: "ComputeInvoice"
# Behavior-preservation + full-propagation gate: if any declaration or caller
# was missed, the test project fails to compile and this assertion fails.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
- type: "output_matches"
pattern: "(dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))"
rubric:
- Migrates in-repository callers to ParseIntegerSetting while preserving the shipped ParseIntSetting entry point as an obsolete forwarding compatibility shim
- Treats PublicAPI.Shipped.txt as evidence that deleting the old method would break a public contract, rather than blindly removing every old-name occurrence
- Rebuilds and re-runs the existing tests after the rename to confirm behavior is preserved
- "Establishes a green build and test baseline before renaming"
- "Renames by tracking true binding references rather than a blind text find-and-replace that would also hit comments, strings, or unrelated test method names"
- "Rebuilds and re-runs the existing tests after the rename to confirm behavior is preserved"
- "Keeps this a single, focused operation without bundling unrelated edits"
timeout: 600
- name: Rename a member declared by a generated partial source
prompt: "Rename the private `Coupons.RateFor` helper to `DiscountRateFor` everywhere it is declared and called, preserving behavior. The solution is at Fixture.sln."
environment: *fixture-environment
graders:
- type: file-contains
config:
path: src/Billing/Coupons.g.cs.template
value: DiscountRateFor
- type: file-contains
config:
path: src/Billing/Coupons.cs
value: DiscountRateFor
- type: file-not-contains
config:
path: src/Billing/Coupons.cs
value: amount * RateFor(code)
- type: run-command
config:
command: dotnet test Fixture.sln --verbosity normal
expected_exit_code: 0
stdout_contains: "Total tests: 10"
timeout: 5m
- type: output-matches
config:
pattern: (generated|generator|template)
- type: output-matches
config:
pattern: (dotnet (build|test)|rebuild|re-?run[\s\S]{0,30}tests|tests?[\s\S]{0,30}(pass|green))
- type: prompt
- name: "Extract a repeated calculation into a private helper"
prompt: "`OrderProcessor.DoStuff` is too long. Extract the discount-then-tax calculation into a private helper method and call it, without changing what the code computes. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
# Behavior-preservation gate: the extracted helper must compute identical
# results, so the existing tests must still pass.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
# A private helper method now exists (OrderProcessor had none before).
- type: "file_contains"
path: "src/Billing/OrderProcessor.cs"
value: "private"
- type: "output_matches"
pattern: "(dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))"
rubric:
- Finds the generated declaration and the hand-authored partial caller as binding-related parts of one rename
- Edits Coupons.g.cs.template, the build's source of truth, rather than a generated file under obj
- Rebuilds and re-runs the tests so generation occurs again and proves the renamed declaration and caller agree
- "Establishes a green baseline before editing"
- "Extracts the block into a new method preserving the exact same computation and results"
- "Verifies behavior is preserved by building and running the existing tests after extracting"
- "Does not slip a bug fix or behavior change into the extraction"
timeout: 600
- name: Consolidate duplicate public helpers without breaking shipped callers
prompt: "`ConfigReader` duplicates the parsing implementation in `AppSettingsHelper`. Consolidate the implementation so there is one source of truth, but preserve the shipped `ConfigReader.ReadInt` and `ReadBool` APIs for existing callers. Migrate this solution's callers where appropriate and verify Fixture.sln."
environment: *fixture-environment
graders:
- type: file-contains
config:
path: src/Billing/AppSettingsHelper.cs
value: AppSettingsHelper.ParseIntSetting
- type: file-contains
config:
path: src/Billing/AppSettingsHelper.cs
value: AppSettingsHelper.ParseBoolSetting
- type: file-contains
config:
path: src/Billing/AppSettingsHelper.cs
value: class ConfigReader
- type: file-contains
config:
path: src/Billing/PublicAPI.Shipped.txt
value: Billing.ConfigReader
- type: run-command
config:
command: dotnet test Fixture.sln --verbosity normal
expected_exit_code: 0
stdout_contains: "Total tests: 10"
timeout: 5m
- type: output-matches
config:
pattern: (public API|compatib|shipped|forward)
- type: output-matches
config:
pattern: (dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))
- type: prompt
- name: "Consolidate a duplicated block into a single shared helper"
prompt: "Inside `OrderProcessor.DoStuff` the discount-plus-tax calculation is duplicated (it appears twice, once for the order total and once for the quote). Consolidate the duplicated logic into a single shared helper used by both, without changing behavior. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
# Behavior-preservation gate: the single shared helper must be equivalent to
# both original copies, so the existing tests must still pass.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
# A shared private helper now exists (OrderProcessor had none before).
- type: "file_contains"
path: "src/Billing/OrderProcessor.cs"
value: "private"
- type: "output_matches"
pattern: "(dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))"
rubric:
- Removes the duplicate parsing implementation by making ConfigReader delegate to AppSettingsHelper
- Preserves the shipped ConfigReader type and methods rather than deleting a public contract during de-duplication
- Re-runs the build and tests after consolidating to prove behavior and existing callers remain intact
- "Establishes a green baseline before editing"
- "Factors the duplicated block into one shared helper that both call sites use"
- "Confirms the consolidated helper is semantically equivalent to each original copy"
- "Re-runs the build and tests after consolidating to prove behavior is unchanged"
timeout: 600
- name: Inline a pass-through wrapper and update callers
- name: "Inline a pass-through wrapper and update callers"
prompt: "`LegacyTax.ApplyTaxWrapper` does nothing but forward to `TaxRules.Apply`. Inline it: update every caller to call `TaxRules.Apply` directly and remove the wrapper, without changing behavior. The solution is at Fixture.sln."
environment: *fixture-environment
graders:
setup:
copy_test_files: true
assertions:
# Behavior-preservation + full-propagation gate: every caller (including the
# test that references the wrapper) must be updated to the underlying call,
# or the test project fails to compile.
- type: run-command
config:
command: dotnet test Fixture.sln --verbosity normal
expected_exit_code: 0
stdout_contains: "Total tests: 10"
timeout: 5m
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
# The pass-through wrapper is actually gone.
- type: file-not-contains
config:
path: src/Billing/Pricing.cs
value: ApplyTaxWrapper
- type: output-matches
config:
pattern: (dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))
- type: prompt
- type: "file_not_contains"
path: "src/Billing/Pricing.cs"
value: "ApplyTaxWrapper"
- type: "output_matches"
pattern: "(dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))"
rubric:
- Finds the true binding references to the wrapper before removing it, not just textual matches
- Updates every caller to the underlying call and removes the wrapper in one focused operation
- Lets the compiler catch any missed reference, then rebuilds and re-runs the tests to confirm green
- "Finds the true binding references to the wrapper before removing it, not just textual matches"
- "Updates every caller to the underlying call and removes the wrapper in one focused operation"
- "Lets the compiler catch any missed reference, then rebuilds and re-runs the tests to confirm green"
timeout: 600
- name: Merge two near-identical types into one parameterized type
- name: "Merge two near-identical types into one parameterized type"
prompt: "`GoldPricing` and `SilverPricing` are near-identical. Consolidate them into a single parameterized pricing type and update any usages, without changing behavior. The solution is at Fixture.sln."
environment: *fixture-environment
graders:
setup:
copy_test_files: true
assertions:
# Behavior-preservation + full-propagation gate: usages (including the test
# that constructs GoldPricing/SilverPricing) must be updated to the merged
# type, or the test project fails to compile.
- type: run-command
config:
command: dotnet test Fixture.sln --verbosity normal
expected_exit_code: 0
stdout_contains: "Total tests: 10"
timeout: 5m
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
# The two near-identical classes are actually gone (merged into one type).
- type: file-not-contains
config:
path: src/Billing/Pricing.cs
value: class GoldPricing
- type: file-not-contains
config:
path: src/Billing/Pricing.cs
value: class SilverPricing
- type: output-matches
config:
pattern: (dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))
- type: prompt
- type: "file_not_contains"
path: "src/Billing/Pricing.cs"
value: "class GoldPricing"
- type: "file_not_contains"
path: "src/Billing/Pricing.cs"
value: "class SilverPricing"
- type: "output_matches"
pattern: "(dotnet (build|test)|rebuild|re-?run.{0,20}tests|tests? .{0,20}(pass|green))"
rubric:
- Merges the two classes into a single type parameterized by name and rate, preserving each tier's values
- Updates all usages and re-runs the build and tests to prove behavior is unchanged
- "Establishes a green baseline before editing"
- "Merges the two classes into a single type parameterized by name and rate, preserving each tier's values"
- "Updates all usages and re-runs the build and tests to prove behavior is unchanged"
timeout: 600
- name: Decline a framework and package upgrade dressed up as a refactor
prompt: Upgrade the Billing project in Fixture.sln to a newer target framework and bump all its NuGet packages to their latest major versions, treating this as a behavior-preserving refactor.
environment: *fixture-environment
graders:
- type: output-matches
config:
pattern: (not a .{0,20}refactor|behavior[- ]?(preserving|chang)|upgrade|major version|out of scope)
- type: prompt
- name: "Decline a framework and package upgrade dressed up as a refactor"
prompt: "Upgrade the Billing project to a newer target framework and bump all its NuGet packages to their latest major versions. The solution is at Fixture.sln."
expect_activation: false
setup:
copy_test_files: true
assertions:
- type: "output_matches"
pattern: "(not a .{0,20}refactor|behavior[- ]?(preserving|chang)|upgrade|major version|out of scope)"
rubric:
- Recognizes that a framework or package major-version upgrade is not a behavior-preserving refactor
- Declines or redirects to the appropriate upgrade workflow instead of treating it as a refactor
- Does not silently perform a behavior-changing version bump under the guise of refactoring
- "Recognizes that a framework or package major-version upgrade is not a behavior-preserving refactor"
- "Declines or redirects to the appropriate upgrade workflow instead of treating it as a refactor"
- "Does not silently perform a behavior-changing version bump under the guise of refactoring"
timeout: 300
- name: Decline a new-feature request dressed up as a refactor
prompt: 'In Fixture.sln, add a new "platinum" loyalty tier at 15% off and wire it into the pricing tiers so orders can use it, treating this as a behavior-preserving refactor.'
environment: *fixture-environment
graders:
- type: output-matches
config:
pattern: (new feature|feature addition|not a .{0,20}refactor|behavior[- ]?(preserving|chang)|adds? new behavior)
- type: prompt
- name: "Decline a new-feature request dressed up as a refactor"
prompt: "Add a new \"platinum\" loyalty tier at 15% off and wire it into the pricing tiers so orders can use it. The solution is at Fixture.sln."
expect_activation: false
setup:
copy_test_files: true
assertions:
- type: "output_matches"
pattern: "(new feature|feature addition|not a .{0,20}refactor|behavior[- ]?(preserving|chang)|adds? new behavior)"
rubric:
- Recognizes that adding a new pricing tier is a new feature, not a behavior-preserving refactor
- Distinguishes the feature request from a refactor rather than conflating them, and does not claim behavior is preserved when it is deliberately being extended
- If it proceeds, does so as an explicit feature addition with its own new tests, not under the guise of a no-behavior-change refactor
- "Recognizes that adding a new pricing tier is a new feature, not a behavior-preserving refactor"
- "Distinguishes the feature request from a refactor rather than conflating them, and does not claim behavior is preserved when it is deliberately being extended"
- "If it proceeds, does so as an explicit feature addition with its own new tests, not under the guise of a no-behavior-change refactor"
timeout: 300
- name: Keep behavior-changing bug fixes out of a behavior-preserving refactor
prompt: "In Fixture.sln, rename `OrderProcessor.DoStuff` to `ComputeInvoice` and, while you are there, change the free-shipping threshold from $100 to $50. Treat the whole change as one behavior-preserving refactor."
environment: *fixture-environment
graders:
- type: file-contains
config:
path: src/Billing/OrderProcessor.cs
value: ComputeInvoice
- type: file-contains
config:
path: tests/Billing.Tests/BillingTests.cs
value: ComputeInvoice
- type: file-contains
config:
path: src/Billing/OrderProcessor.cs
value: subtotal >= 100m
- type: output-matches
config:
pattern: (separate|follow[- ]?up|behavior[- ]?(preserving|chang)|not (a )?refactor)
- type: output-matches
config:
pattern: (shipping|threshold|\$50)
- type: run-command
config:
command: dotnet test Fixture.sln --verbosity normal
expected_exit_code: 0
stdout_contains: "Total tests: 10"
timeout: 5m
- type: prompt
- name: "Keep behavior-changing bug fixes out of a behavior-preserving refactor"
prompt: "Fix the known bugs first and keep any refactors in a separate follow-up PR. Do not mix behavior-changing bug fixes with refactoring under a behavior-preserving contract. The solution is at Fixture.sln."
expect_activation: false
setup:
copy_test_files: true
assertions:
- type: "output_matches"
pattern: "(separate|follow[- ]?up|bug fix|behavior[- ]?(preserving|chang)|not (a )?refactor)"
rubric:
- Completes the requested rename as a focused behavior-preserving refactor and migrates its callers
- Refuses or defers the $50 shipping-threshold change because it deliberately changes invoice behavior and belongs in a separate tested change
- Keeps the existing $100 threshold and does not claim the combined request is behavior-preserving
- "Recognizes that a behavior-changing bug fix is distinct from a behavior-preserving refactor"
- "Keeps the bug fix separate from any refactoring rather than bundling them under one behavior-preserving contract"
- "Does not claim behavior is preserved for a change that deliberately alters behavior"
timeout: 300
@@ -4,6 +4,7 @@
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<IsPackable>true</IsPackable>
</PropertyGroup>
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Billing", "src\Billing\Billing.csproj", "{11111111-1111-1111-1111-111111111111}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Billing.Tests", "tests\Billing.Tests\Billing.Tests.csproj", "{22222222-2222-2222-2222-222222222222}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
@@ -0,0 +1,165 @@
scenarios:
- name: "Answer a public-API question before consolidating duplicated parsing"
prompt: "Is `AppSettingsHelper` part of the public API anywhere? Its int/bool setting parsing looks duplicated with `ConfigReader`. If it's safe to do, consolidate the setting parsing into one place and update the callers, without changing behavior. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
- type: "output_matches"
pattern: "(public API|PublicAPI\\.Shipped|breaking change|shipped)"
- type: "file_contains"
path: "src/Billing/PublicAPI.Shipped.txt"
value: "AppSettingsHelper"
# Public-surface preservation gate: the shipped members are exercised by the
# tests (AppSettingsHelper + ConfigReader). A "safe" consolidation keeps them
# working; silently removing/renaming a shipped public member breaks the test
# compile and fails here.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
rubric:
- "Answers the public-API question by actually inspecting what governs the surface (finds and reads `PublicAPI.Shipped.txt`, which lists the public parsing members) rather than assuming"
- "Recognizes that removing or renaming a shipped public member is a breaking change a green test run will not catch, and either preserves the public surface (for example keeps the members as thin forwarders) or explicitly flags the break and updates the API baseline — does not silently break the public API"
- "If it consolidates, keeps behavior unchanged and re-runs the build and tests to prove it"
timeout: 600
- name: "Add behavior to a shipped public member without breaking the contract"
prompt: "Add hex support to `AppSettingsHelper.ParseIntSetting` so callers can parse values like `0x1F` in addition to plain decimals. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
- type: "output_matches"
pattern: "(public API|PublicAPI|breaking change|shipped|signature|overload)"
- type: "file_contains"
path: "src/Billing/PublicAPI.Shipped.txt"
value: "ParseIntSetting"
# The shipped signature must not break: existing decimal-parsing tests must
# still pass after hex support is added.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
rubric:
- "Treats this as a feature (adds new hex-parsing behavior), not a behavior-preserving refactor, and does not claim behavior is unchanged"
- "Recognizes `AppSettingsHelper.ParseIntSetting` is a shipped public member listed in `PublicAPI.Shipped.txt`, and that changing its signature or return type would be a breaking change a green test run will not catch"
- "Adds hex support without breaking the shipped signature — extends the body or adds an overload — and if the public surface changes, updates the API baseline (`PublicAPI.Unshipped.txt`) or explicitly flags the break rather than silently altering the shipped contract"
- "Keeps the existing tests green and ideally adds coverage for the hex case"
timeout: 600
- name: "Rename a helper referenced from every #if branch of a multi-targeted type"
prompt: "The private `Label` helper in `PlatformInfo` is poorly named — rename it to `FormatLabel`. Keep behavior identical. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
- type: "file_contains"
path: "src/Billing/PlatformInfo.cs"
value: "FormatLabel"
# Multi-target gate: building the whole solution compiles BOTH net8.0 and
# net10.0. `Label` is referenced from both #if branches, so if the rename
# missed the branch the default build doesn't compile, the net8.0 build fails.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "build Fixture.sln -v:q"
expected_exit_code: 0
command_timeout: 300
# Behavior-preservation gate on the built target.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
- type: "output_matches"
pattern: "(multi[- ]?target|#if|net8|net10|target framework|TFM|every (branch|target))"
rubric:
- "Recognizes the project multi-targets (`net8.0;net10.0`) and that `Label` is referenced inside BOTH `#if` branches, so the rename must be replicated across every `#if` branch and target framework — not just the branch the default build compiles"
- "Renames by tracking true binding references, then rebuilds ALL target frameworks and re-runs the tests to prove behavior is preserved"
- "Keeps this a single, focused rename without bundling unrelated edits"
timeout: 600
- name: "Rename a member of a partial type that a generated part references"
prompt: "Rename `Coupons.Redeem` to `Apply` — the name is too generic. Keep behavior identical. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
- type: "file_contains"
path: "src/Billing/Coupons.cs"
value: "Apply"
# Positive check that the fix went to the GENERATOR INPUT (the template),
# which is where the generated part's reference to the renamed member lives.
# Hand-editing the obj/ output would be overwritten on the next build.
- type: "file_contains"
path: "src/Billing/Coupons.g.cs.template"
value: "Apply"
# Full-propagation gate across the partial type: building the solution
# regenerates the partial from the template. If the template (or any caller)
# still references the old name, the regenerated code fails to compile.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
- type: "output_matches"
pattern: "(auto[- ]?generated|source generator|generated (file|output|code)|partial|template|\\.g\\.cs|regenerat)"
rubric:
- "Includes EVERY partial declaration of `Coupons` when renaming, and finds the reference in the generated part (which references the renamed member)"
- "Recognizes the generated part is emitted at build time from `Coupons.g.cs.template`: flags that hand-editing the generated output under `obj/` is unsafe (it is overwritten on the next build) and that the correct fix edits the template (the generator input) — not the emitted file"
- "Rebuilds and re-runs the tests, letting the compiler catch any missed reference, to prove behavior is preserved"
timeout: 600
- name: "Add a new public method that must be correct on every target framework"
prompt: "Add a new public method `PlatformInfo.Tag()` that returns just the short platform tag (for example `net10` or `net8`) without the `platform:` prefix. It must return the correct tag on every target framework. The solution is at Fixture.sln."
setup:
copy_test_files: true
assertions:
- type: "file_contains"
path: "src/Billing/PlatformInfo.cs"
value: "Tag"
# Multi-target gate: the new method must compile on BOTH net8.0 and net10.0.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "build Fixture.sln -v:q"
expected_exit_code: 0
command_timeout: 300
# Existing behavior stays green after the additive change.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
- type: "output_matches"
pattern: "(multi[- ]?target|#if|net8|net10|target framework|TFM|public API|PublicAPI)"
rubric:
- "Treats this as a feature addition (a new public method), not a behavior-preserving refactor"
- "Recognizes the project multi-targets (`net8.0;net10.0`) and uses `#if` branches, and implements `Tag()` so it returns the correct value on EVERY target framework — building and testing each target, not just the one the default build compiles"
- "Recognizes `Tag()` is a NEW public member of a shipped type (public API surface) and updates the API baseline (`PublicAPI.Unshipped.txt`) or flags the surface addition"
- "Keeps existing tests green and adds coverage for the new method"
timeout: 600
- name: "Do not raise breaking-change concerns for a purely internal local rename"
prompt: "Inside `OrderProcessor.DoStuff`, the local variable `subtotal` is a bit terse. Rename that local to `runningTotal` for readability. Keep behavior identical. The solution is at Fixture.sln."
expect_activation: false
setup:
copy_test_files: true
assertions:
- type: "file_contains"
path: "src/Billing/OrderProcessor.cs"
value: "runningTotal"
# The local rename is behavior-identical: tests must still pass unchanged.
- type: run_command_and_assert
command_to_run: "dotnet"
command_arguments: "test Fixture.sln -v:q"
expected_exit_code: 0
expected_std_output_contains: "Passed!"
command_timeout: 300
rubric:
- "Recognizes that renaming a method-local variable touches no public API surface, no `#if`/multi-target branch, and no generated code, so no breaking-change hazard applies"
- "Performs the local rename directly without introducing public-API baseline edits, source-generator changes, or other breaking-change ceremony that the change does not warrant"
- "Confirms behavior is unchanged by rebuilding and re-running the tests"
timeout: 300
@@ -0,0 +1,21 @@
namespace Billing;
/// <summary>Parses application settings, returning a fallback when the raw value is missing or invalid.</summary>
public static class AppSettingsHelper
{
public static int ParseIntSetting(string? raw, int fallback)
=> int.TryParse(raw, out var v) ? v : fallback;
public static bool ParseBoolSetting(string? raw, bool fallback)
=> bool.TryParse(raw, out var v) ? v : fallback;
}
/// <summary>Reads configuration values, returning a fallback when the raw value is missing or invalid.</summary>
public static class ConfigReader
{
public static int ReadInt(string? raw, int fallback)
=> int.TryParse(raw, out var v) ? v : fallback;
public static bool ReadBool(string? raw, bool fallback)
=> bool.TryParse(raw, out var v) ? v : fallback;
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Include="Coupons.g.cs.template" />
</ItemGroup>
<Target Name="GenerateCouponsPartial" BeforeTargets="CoreCompile">
<Copy SourceFiles="Coupons.g.cs.template"
DestinationFiles="$(IntermediateOutputPath)Coupons.g.cs"
SkipUnchangedFiles="true" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)Coupons.g.cs" />
</ItemGroup>
</Target>
</Project>
@@ -0,0 +1,8 @@
namespace Billing;
/// <summary>Hand-authored part of the <c>Coupons</c> partial type.</summary>
public partial class Coupons
{
public decimal Redeem(string code, decimal amount)
=> amount - (amount * RateFor(code));
}
@@ -0,0 +1,14 @@
// <auto-generated/>
namespace Billing;
public partial class Coupons
{
private static decimal RateFor(string code) => code switch
{
"SAVE10" => 0.10m,
"SAVE05" => 0.05m,
_ => 0.00m,
};
public decimal RedeemDefault(decimal amount) => Redeem("SAVE10", amount);
}
@@ -0,0 +1,36 @@
namespace Billing;
/// <summary>A single line item on an order.</summary>
public readonly record struct OrderLine(string Sku, decimal UnitPrice, int Quantity);
/// <summary>The computed result of pricing an order.</summary>
public readonly record struct Invoice(decimal Total, decimal Quote, decimal Shipping);
public sealed class OrderProcessor
{
public Invoice DoStuff(IReadOnlyList<OrderLine> lines, string tier)
{
decimal subtotal = 0m;
foreach (var line in lines)
{
subtotal += line.UnitPrice * line.Quantity;
}
decimal shipping = subtotal >= 100m ? 0m : 9.99m;
decimal rateA = tier == "gold" ? 0.10m : tier == "silver" ? 0.05m : 0.00m;
decimal discountedA = subtotal - (subtotal * rateA);
decimal totalWithTax = discountedA + (discountedA * 0.08m);
decimal quoteBase = subtotal + shipping;
decimal rateB = tier == "gold" ? 0.10m : tier == "silver" ? 0.05m : 0.00m;
decimal discountedB = quoteBase - (quoteBase * rateB);
decimal quoteWithTax = discountedB + (discountedB * 0.08m);
return new Invoice(
Math.Round(totalWithTax, 2),
Math.Round(quoteWithTax, 2),
shipping);
}
}
@@ -0,0 +1,19 @@
namespace Billing;
/// <summary>Reports a platform label for the current target framework.</summary>
public static class PlatformInfo
{
private const string ModernTag = "net10";
private const string LegacyTag = "net8";
public static string Current()
{
#if NET8_0
return Label(LegacyTag);
#else
return Label(ModernTag);
#endif
}
private static string Label(string tag) => $"platform:{tag}";
}
@@ -0,0 +1,25 @@
namespace Billing;
/// <summary>The canonical tax rule.</summary>
public static class TaxRules
{
public static decimal Apply(decimal amount) => amount + (amount * 0.08m);
}
/// <summary>Legacy tax entry point retained for older callers.</summary>
public static class LegacyTax
{
public static decimal ApplyTaxWrapper(decimal amount) => TaxRules.Apply(amount);
}
public sealed class GoldPricing
{
public decimal Rate => 0.10m;
public string Name => "gold";
}
public sealed class SilverPricing
{
public decimal Rate => 0.05m;
public string Name => "silver";
}
@@ -0,0 +1,7 @@
#nullable enable
Billing.AppSettingsHelper
static Billing.AppSettingsHelper.ParseIntSetting(string? raw, int fallback) -> int
static Billing.AppSettingsHelper.ParseBoolSetting(string? raw, bool fallback) -> bool
Billing.ConfigReader
static Billing.ConfigReader.ReadInt(string? raw, int fallback) -> int
static Billing.ConfigReader.ReadBool(string? raw, bool fallback) -> bool
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Billing\Billing.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,111 @@
using Billing;
using Xunit;
namespace Billing.Tests;
public class BillingTests
{
[Fact]
public void Invoice_NoTier_ComputesTotalAndQuote()
{
var processor = new OrderProcessor();
var lines = new[]
{
new OrderLine("A", 50m, 1),
new OrderLine("B", 20m, 2),
};
var invoice = processor.DoStuff(lines, "none");
// subtotal = 90, shipping = 9.99 (subtotal < 100)
// total = 90 * 1.08 = 97.20
// quote = (90 + 9.99) * 1.08 = 107.9892 -> 107.99
Assert.Equal(97.20m, invoice.Total);
Assert.Equal(107.99m, invoice.Quote);
Assert.Equal(9.99m, invoice.Shipping);
}
[Fact]
public void Invoice_Gold_AppliesDiscountAndFreeShipping()
{
var processor = new OrderProcessor();
var lines = new[] { new OrderLine("A", 100m, 2) };
var invoice = processor.DoStuff(lines, "gold");
// subtotal = 200, shipping = 0 (subtotal >= 100)
// discounted = 200 * 0.90 = 180, total = 180 * 1.08 = 194.40
// quote base = 200 -> same as total = 194.40
Assert.Equal(194.40m, invoice.Total);
Assert.Equal(194.40m, invoice.Quote);
Assert.Equal(0m, invoice.Shipping);
}
[Fact]
public void Invoice_Silver_AppliesFivePercentDiscount()
{
var processor = new OrderProcessor();
var lines = new[] { new OrderLine("A", 200m, 1) };
var invoice = processor.DoStuff(lines, "silver");
// subtotal = 200, shipping = 0
// discounted = 200 * 0.95 = 190, total = 190 * 1.08 = 205.20
Assert.Equal(205.20m, invoice.Total);
Assert.Equal(205.20m, invoice.Quote);
}
[Fact]
public void Tax_Wrapper_MatchesUnderlyingRule()
{
Assert.Equal(TaxRules.Apply(100m), LegacyTax.ApplyTaxWrapper(100m));
Assert.Equal(108m, LegacyTax.ApplyTaxWrapper(100m));
}
[Fact]
public void PricingTiers_HaveExpectedRatesAndNames()
{
Assert.Equal(0.10m, new GoldPricing().Rate);
Assert.Equal("gold", new GoldPricing().Name);
Assert.Equal(0.05m, new SilverPricing().Rate);
Assert.Equal("silver", new SilverPricing().Name);
}
[Fact]
public void PlatformInfo_Current_ReportsModernTagUnderNet10()
{
// The test project targets net10.0, so the #else branch is active.
Assert.Equal("platform:net10", PlatformInfo.Current());
}
[Fact]
public void AppSettingsHelper_ParsesOrFallsBack()
{
Assert.Equal(42, AppSettingsHelper.ParseIntSetting("42", 0));
Assert.Equal(7, AppSettingsHelper.ParseIntSetting("nope", 7));
Assert.True(AppSettingsHelper.ParseBoolSetting("true", false));
Assert.False(AppSettingsHelper.ParseBoolSetting("bad", false));
}
[Fact]
public void ConfigReader_MatchesAppSettingsHelper()
{
Assert.Equal(AppSettingsHelper.ParseIntSetting("10", 0), ConfigReader.ReadInt("10", 0));
Assert.Equal(AppSettingsHelper.ParseBoolSetting("true", false), ConfigReader.ReadBool("true", false));
}
[Fact]
public void Coupons_Redeem_AppliesRate()
{
var coupons = new Coupons();
Assert.Equal(90m, coupons.Redeem("SAVE10", 100m));
Assert.Equal(95m, coupons.Redeem("SAVE05", 100m));
Assert.Equal(100m, coupons.Redeem("UNKNOWN", 100m));
}
[Fact]
public void Coupons_RedeemDefault_UsesSave10()
{
Assert.Equal(90m, new Coupons().RedeemDefault(100m));
}
}