[PLAN-0057] Compono.MSTest Package Design — Implementation Plan¶
Status: Done
Implements: ADR-0057 including Amendment 1 (MSTest.TestFramework minimum raised from 3.0.0 to 4.0.0 — the two lines ship under different, binary-incompatible assembly identities, found during this plan's own implementation) (Compono.MSTest package: ITestDataSource-implementing [Compose] attribute family, MSTest.TestFramework-only dependency at a 4.0.0 floor, CompositionRow/RowInvokerRegistry reused unchanged, the BindingPlan/RowInvokers pattern adapted package-locally rather than extracted, GetDisplayName-based seed reporting, the documented discovery/execution repeat-composition contract, no TestContext auto-injection, no disposal ownership)
Note: ADR-0057 is Accepted as of this plan's creation (2026-09-02) — implementation may begin. Amendment 1 (2026-09-02) raised the MSTest.TestFramework floor from 3.0.0 to 4.0.0 after implementation evidence showed the 3.x/4.x lines are binary-incompatible; see this plan's own Notes for the implementation-time finding that triggered it.
Goal¶
[TestClass]
public class OrderServiceTests
{
[TestMethod]
[Compose<NSubstituteTestProfile>]
public void Saves_order(
[Shared] IOrderRepository repository,
CreateOrderHandler handler,
PlaceOrder command)
{
handler.Handle(command);
repository.Received(1).Save(Arg.Any<Order>());
}
}
runs end-to-end under a real MSTest test host — the same scenario Compono.XunitV3.SampleTests.NSubstituteTests.Saves_order/ Compono.TUnit.SampleTests.NSubstituteTests.Saves_order already prove, reproduced a third time under [TestMethod] + [Compose], with no [DataTestMethod], repository shared across handler's own composed constructor parameter via [Shared], the row's seed visible in GetDisplayName's output under both MTP and the classic VSTest adapter, and a generated-code-reachability proof that a type reached only through a Compono.MSTest-attributed method's parameter still gets a generated composition plan. Done means: Compono.MSTest.nupkg builds and packs, every ADR-0057 behavioral contract (§1-§16) has a passing test or a recorded real-run verification proving it, Compono.MSTest is reflection- free/AOT-clean on the same terms as Compono.XunitV3/Compono.TUnit, every documentation/skill/eval surface ADR-0057 §16 names is updated and consistent with the shipped API, and a dedicated external MSTest packaged-consumer validation fixture has been validated via scripts/dogfood-validate.sh against freshly packed local packages — see task group 15's own terminology note: this is real, pre-1.0 external consumption validation, not product dogfooding (no real LayeredCraft/ncipollina MSTest consumer exists to dogfood against yet).
Scope¶
Per ADR-0057's Decision Outcome — carried forward exactly, not reopened unless implementation exposes a genuine contradiction:
In scope: a new Compono.MSTest package (ComposeAttribute/ ComposeAttribute<TProfile>/ComposeAttribute<TProfile, TConfig>/ SharedAttribute, §6's frozen public shape), its package-local BindingPlan/ParameterBindingPlan/PositionalArgumentBinder/ ConfigProfileBinder/RowInvokers binding implementation (adapted from Compono.XunitV3's, dispatching through the existing, unchanged RowInvokerRegistry/CompositionRow), the three-metadata-name Compono.Generators discovery extension (§5 "Generator discovery"), MTP and classic-VSTest-adapter support, GetDisplayName-based seed reporting, the documented discovery/execution repeat-composition contract (§9), [DataRow]/[DynamicData] independent-row coexistence (§10), the full documentation/skill/eval/dogfooding completion-gate work ADR-0057 §16 requires as part of this feature's definition of done, and all repository build/CI/packaging integration a new package needs.
Explicitly deferred / non-goals (per ADR-0057's own Deferred Decisions section — not this plan's job to solve): per-parameter mixing of [DataRow]/[DynamicData] with [Compose]; class/assembly-level [Compose<TProfile>]; automatic TestContext/framework-value injection; a supported deferred/lazy ITestDataSource evaluation mechanism; async composition; extracting a shared BindingPlan/RowInvokers base across Compono.XunitV3/Compono.TUnit/Compono.MSTest (revisit only if a fourth framework, e.g. NUnit, is researched and shows the pattern actually generalizes — not a task in this plan); any Compono.MSTest-owned disposal mechanism; raising the 4.0.0 floor further without a concrete missing-capability finding (the floor was already raised once, from 3.0.0, by ADR-0057 Amendment 1's binary-incompatibility finding — see this plan's Notes).
One PR for everything inside this repository. Task groups 1-14 below — the Compono.MSTest runtime package, its Compono.Generators discovery extension, the full test suite, Native AOT/MTP/VSTest validation, and the complete documentation/skill/eval synchronization — are one cohesive feature with one public API boundary and one definition of done; they ship as one PR, not split into phase-per-PR slices the way PLAN-0040 was (that plan predates this repo's current "don't manufacture PR boundaries out of a single package feature" guidance — see ADR-0057's own request framing). Task group 15 (the external MSTest packaged-consumer validation fixture) is, by its nature, a separate repository's own history and cannot land in this repo's PR at all — it follows PLAN-0055 task 18's precedent: this plan's Status: Done still requires it to be substantially complete, but its own commits live outside this repo.
Tasks¶
1. Package/project creation¶
-
src/Compono.MSTest/Compono.MSTest.csproj—net8.0;net9.0;net10.0;net11.0(ADR-0038's TFM window, matching every other integration package).ProjectReferenceto..\Compono\Compono.csproj(PrivateAssets="none", per the realCompono.XunitV3/Compono.TUnitpackaging lesson PLAN-0004 Phase 3/PLAN-0040 Phase 0 both record — do not repeat that mistake) andPackageReferencetoMSTest.TestFrameworkonly — noMSTest, noMSTest.TestAdapter, noMicrosoft.NET.Test.Sdk(ADR-0057 §7, floor4.0.0per Amendment 1). SamePinProjectReferenceVersionsExactMSBuild target every other integration project's.csprojcarries.InternalsVisibleToforCompono.MSTest.Tests. -
Directory.Packages.props: addMSTest.TestFramework'sPackageVersionentry pinned at (or compatible with) the4.0.0floor ADR-0057 §7 (Amendment 1) accepts, andCompono.MSTest's own entry (Version="1.0.0", matching every other integration package's existing pattern — needed the moment any in-repo consumer project restores a realPackageReferenceto it, not just for the packed artifact itself). -
test/Compono.MSTest.Tests/Compono.MSTest.Tests.csproj— this project executes as a real MSTest test run, so it needs the full test-execution chainsrc/Compono.MSTestdeliberately doesn't carry:PackageReferencetoCompono.MSTest(ProjectReferencein-repo) plusMSTest(the umbrella package) andMicrosoft.NET.Test.Sdk. Confirm the exact required properties (MTP-runner opt-in/out,<UseVSTest>) against a real MSTest project template during implementation rather than guessing them here — both runner paths need to be exercisable from this project or a sibling (task group 8). AddDirectory.Packages.propsentries forMSTest/Microsoft.NET.Test.SdkalongsideMSTest.TestFramework. Add aCompono.MSTest.Tests-name exclusion totest/Directory.Build.props's twoIsTestProject-scopedItemGroups (the shared xUnit-v3-runner packages/globalusing Xunit;set that every other test project gets by default don't belong in an MSTest-run project — mirrors theCompono.TUnit.Testsexclusion PLAN-0040 Phase 0 added). -
Compono.slnx: add both new projects.
2. Public API surface (ADR-0057 §6, frozen — no deviation without stopping to report)¶
-
ComposeAttribute : Attribute, ITestDataSource—public ComposeAttribute(params object?[] inlineValues);public int Seed { get; set; }(non-negative-only contract, same asCompono.XunitV3/Compono.TUnit);IEnumerable<object?[]> GetData(MethodInfo methodInfo);string? GetDisplayName(MethodInfo methodInfo, object?[]? data).[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]. Does not derive from any MSTest attribute base type (§3). -
ComposeAttribute<TProfile> : ComposeAttribute where TProfile : ICompositionProfile, new()— inline-value constructor pass-through,sealed. Confirm the inline-value constructor ships on the base type in this same task group, not deferred to a later one — moving it after a parameterless-only base ships would be a binary-compatibility break for anything already compiled against it (the exact mistake PLAN-0040's sixth Codex review round caught and fixed forCompono.TUnit— do not repeat it here). -
ComposeAttribute<TProfile, TConfig> : ComposeAttribute where TProfile : ICompositionProfile(nonew()constraint) —public ComposeAttribute(params object?[] configArguments) : base()(zero inline values passed to the base;configArgumentsstored in its own field, bound toTConfig's single public constructor via a package-localConfigProfileBinder, thenTProfileconstructed from thatTConfiginstance and applied viaCompositionBuilder.AddProfile). Every test-method parameter is composed in full under this form —configArgumentsnever bind to test-method parameters. Negative-seed validation runs before any config/profile binding is attempted (matchingCompono.XunitV3'sApplyProfileordering, so a badTConfig/TProfileshape combined with a negative configured seed reports the documented negative-seed diagnostic, not a binder failure with a bogus seed embedded). -
SharedAttribute—[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)], package-local marker mirroringCompono.XunitV3.SharedAttribute's shape and duplicate-shared-type validation. - Stacked Compose-family attribute validation: reject a method carrying more than one of
[Compose]/[Compose<TProfile>]/[Compose<TProfile, TConfig>]—AllowMultiple = falseis per-exact-type only, so nothing else stops two different Compose-family types stacking on one method.MethodInfois directly available here (ITestDataSource.GetData(MethodInfo methodInfo)hands it over directly, unlikeCompono.TUnit'sDataGeneratorMetadataindirection) — no reflection workaround needed to find it, unlike PLAN-0040 Phase 1'sResolveMethodInfofallback. -
test/Compono.MSTest.Tests: API-surface/approval test locking the exact four-type public shape (ComposeAttribute,ComposeAttribute`1,ComposeAttribute`2,SharedAttribute), matchingCompono.XunitV3.Tests'/Compono.TUnit.Tests' existing pattern.
3. Binding implementation (src/Compono.MSTest/Binding/*)¶
-
BindingPlan.cs/ParameterBindingPlan.cs/PositionalArgumentBinder.cs— a package-local port ofCompono.XunitV3's own (ADR-0057's binding-logic decision: adapted, not shared core), operating onMethodInfo/ParameterInfo— the same input shapeCompono.XunitV3already uses, so this is a closer, more direct port thanCompono.TUnit'sDataGeneratorMetadata-based version was. Covers: parameter discovery viaMethodInfo.GetParameters(),[Shared]detection, nullability inference, inline-value positional binding/precedence, generic-method rejection,ref/out/in/paramsrejection,ref struct/pointer-typed by-value-parameter rejection (the same dispatch-eligibility guard ADR-0041/PLAN-0041 added toCompono.XunitV3/Compono.TUnit's ownBindingPlan— carry it here from the start, don't rediscover it), duplicate-[Shared]-type rejection, more-than-one-Compose-family-attribute rejection (task group 2's last item). -
RowInvokers.cs— built against coreCompono's existingRowInvokerRegistry.TryGetfrom its first commit (per ADR-0041). No throwawayMakeGenericMethod/Delegate.CreateDelegate-based version ships first —RowInvokerRegistryalready exists and is stable onmain, so unlike PLAN-0040 (which had to wait on PLAN-0041 to merge first), this package can be built against it directly from the start. -
ConfigProfileBinder.cs— package-local port ofCompono.XunitV3's (constructor-shape lookup forTConfig/TProfilevia reflection, bounded to once per attribute instance by the sameLazy<Composer>-backed caching pattern, never on the repeated per-GetData-call path). Unsupported constructor shapes are a deterministicCompositionException, not a compile error, matchingCompono.XunitV3's documented behavior exactly. -
ComposeAttribute.GetData(MethodInfo): oneCompositionRowper invocation (composer.CreateRow(...)), binds every parameter viarow.Resolve<T>()/row.ResolveShared<T>()/row.ShareExplicit<T>()throughRowInvokers, returns oneobject?[]row wrapped in a single-elementIEnumerable<object?[]>(ITestDataSource.GetData's contract — confirm during implementation whether MSTest expects exactly one row per[Compose]invocation or tolerates/requires a different cardinality; ADR-0057's target experience assumes one row per method). No graph state shared across separateGetDatacalls — no static/module-level row cache of any kind (ADR-0057 §9's contract depends on this). - Every
Resolve/ResolveShared/ShareExplicitcall wrapped to catchCompositionExceptionand rethrow viaCompositionException.WithSeedInMessage(exception, row.Seed)— the same unconditional, pasteable-seed guaranteeCompono.XunitV3/Compono.TUnitalready make (ADR-0057 §15). -
test/Compono.MSTest.Tests: binding-plan unit coverage mirroringCompono.XunitV3.Tests' own — parameter resolution, inline-value precedence,[Shared]/duplicate-[Shared]-type validation, nullability, signature-validation errors (generic method,ref/out/in/params,ref struct/pointer by-value), stacked-attribute rejection,CompositionExceptionseed enrichment. Hand-builtMethodInfo/ParameterInfofixtures are directly usable here (unlikeCompono.TUnit.Tests, which neededMethodMetadataFactory— MSTest'sGetData(MethodInfo)takes exactly what ordinary reflection already produces).
4. Generator discovery (Compono.Generators)¶
-
src/Compono.Generators/Discovery/ComposeMethodDiscovery.cs,src/Compono.Generators/ComponoIncrementalGenerator.cs: three new metadata-name constants (Compono.MSTest.ComposeAttribute/`1/`2) and three newSyntaxValueProvider .ForAttributeWithMetadataNameregistrations, feeding the existing, already attribute-family-agnosticComposeMethodDiscovery .TransformMethod— no fork or reimplementation of that method (ADR-0057 §5 "Generator discovery"). -
test/Compono.Generators.Tests: a snapshot test proving a concrete parameter type reachable only through aCompono.MSTest-attributed method's own parameter (no other discovery path in the same compilation) receives a generated composition plan and aRowInvokerRegistryregistration — mirroring the equivalentCompono.XunitV3/Compono.TUnitregression coverage exactly. - A minimal local-feed packaged-consumer proof of this specific change, in this task group, not deferred — a
dotnet pack→ local-feed → real restore consumer (mirroring PLAN-0004 Phase 3/PLAN-0040 Phase 0's precedent) using the unqualified[Compose]attribute against a type with no other discovery path, proving the packedCompono.MSTest/Componodependency chain actually generates a plan for it — a snapshot test alone doesn't prove the real NuGet dependency chain works. Task group 9's packaged-consumer sample project can absorb this once it exists, but this task group needs its own minimal proof if sequencing puts generator work first.
5. Display name and seed diagnostics (ADR-0057 §15)¶
-
GetDisplayName(MethodInfo methodInfo, object?[]? data)returns"{methodName} (Compono, seed: {seed})"— the row's actual seed, not a placeholder. NoTestContext.Properties/TestPropertyusage for the primary seed-reporting path. -
test/Compono.MSTest.Tests: unit coverage thatGetDisplayName's output contains the exact seed a given[Compose(Seed = N)]/ generated-seed row used. - Real-run verification (task group 8) that the display name is actually visible in
dotnet test/Test Explorer output under both MTP and the classic VSTest adapter — a design-time claim confirmed against a real runner, not assumed from the API contract alone.
6. [DataRow]/[DynamicData] coexistence (ADR-0057 §10)¶
-
test/Compono.MSTest.Testsor the sample project (task group 9): a method carrying both[DataRow(...)]and[Compose]produces two independent test cases, each supplying a complete row for every parameter — documents/proves the boundary rather than building any merging machinery. No new execution-layer code needed; this is a structural property of MSTest's ownITestDataSourcemodel.
7. Discovery/execution repeat-composition behavior (ADR-0057 §9)¶
- Do not build any workaround — no cross-process caching, no global/static row cache, no value serialization between discovery and execution, no custom
TestMethodAttribute. Tests verify Compono's own semantics only: eachGetDatacall gets a fresh, independentCompositionRow;[Shared]/Share<T>()stay correct within one call; two independently-composed rows for the same seed are logically equivalent in value even though they're distinct instances; an observable side effect in aRegister<T>()factory orICompositionValueProvidergenuinely repeats ifGetDatais called more than once. - A real-run check (not a permanently-asserted unit test, since this is an MSTest-runner-workflow property, not something
Compono.MSTest's own code controls): confirm, once, the RESEARCH-0017 §20/§20a evidence still holds against whatever current MSTest/MTP/VSTest versions this plan actually builds against — singledotnet testunder MTP: oneGetDatacall; singledotnet testunder classic VSTest: one call; separatedotnet test --list-tests(discovery) +dotnet test(execution) under classic VSTest: two calls. Record the actual observed result in this plan's Notes, matching PLAN-0040's own "prove once via a real run, then keep only a passing regression" pattern for its own runner-lifecycle finding. - Package README/skill content (task groups 11-12) states this contract as
Compono.MSTest's own documented behavior, not just this ADR's.
8. MTP/VSTest and version-compatibility validation¶
- Confirm
Compono.MSTestworks correctly under both MTP (thedotnet new mstestdefault) and the classic VSTest adapter (<UseVSTest>true</UseVSTest>) — real runs, not assumed fromITestDataSourcebeing runner-agnostic on paper.Compono.MSTestitself introduces no runner-selection logic or MTP-/VSTest-specific API of any kind; runner choice stays entirely the consumer project's configuration. - Validate the accepted
MSTest.TestFramework4.0.0floor (ADR-0057 §7 Amendment 1) actually restores and runs correctly fortest/Compono.MSTest.Tests(or a dedicated compatibility-matrix sibling project/CI leg —implement.md's call which), against both MTP and the classic VSTest adapter. Do not raise the floor further merely because a later4.xAPI would be convenient — if implementation genuinely cannot satisfy the ADR within the4.0.0capability set, stop and report the exact missing API/behavior before touching the floor again, the same discipline that produced Amendment 1 itself. - Validate current
MSTest.TestFramework4.x(the latest patch as of implementation time, e.g.4.3.3) against both MTP and the classic VSTest adapter — both ends of the4.xline must pass the same behavioral suite.MSTest.TestFramework3.xis not validated or supported (ADR-0057 §7 Amendment 1) — do not add a3.xcompatibility leg. - The full compatibility matrix this task group validates:
MSTest.TestFramework4.0.0× MTP,4.0.0× classic VSTest, current4.x× MTP, current4.x× classic VSTest. If the floor and "current" happen to be the same version at implementation time, note that explicitly rather than silently running a redundant fifth/sixth leg. - Record the actual compatibility matrix exercised (MSTest version × MTP/VSTest) in this plan's Notes once run for real.
9. Packaged-consumer sample project (test/Compono.MSTest.SampleTests)¶
- A real packaged-consumer project (mirroring PLAN-0004 Phase 3/ PLAN-0040 Phase 0/2's precedent exactly) exercising the complete attribute family (
[Compose]/[Compose<TProfile>]/[Compose<TProfile, TConfig>]/[Shared]) through the actual packagedCompono.MSTest→Componodependency chain, notCompono.MSTest.Tests' ownProjectReference-based calls — aProjectReferencedoesn't propagateCompono.Generatorsas an analyzer the way a packed nupkg'sanalyzers/dotnet/csdelivery does (the same constrainttesting.mddocuments and PLAN-0040 Phase 0 hit directly), so any test needing a real generated plan for a genuinely composed custom type belongs here, not inCompono.MSTest.Tests. - Includes the
NSubstituteTests.Saves_order-shaped scenario from this plan's Goal section, run for real (needsCompono.NSubstituteas an additional project dependency, matching the xUnit v3/TUnit sample projects). - Includes
ConfigProfileTests-shaped coverage for[Compose<TProfile, TConfig>], mirroringCompono.XunitV3.SampleTests/Compono.TUnit.SampleTests.
10. Native AOT / reflection-free validation¶
- Confirm
Compono.MSTest's own code contains noMakeGenericType,Activator.CreateInstance(beyondConfigProfileBinder's existing, already-accepted bounded reflection pattern — see below),MakeGenericMethod, or reflection-based fallback construction outside theMethodInfo/ParameterInfometadata access `ADR-0057 §14 explicitly accepts as framework-required, not a new reflection category. -
ConfigProfileBinder'sTConfig/TProfileconstruction needs the same AOT gate ADR-0041 Amendment 1 already required forCompono.TUnit's equivalent binder — verify for real (don't assumeConstructorInfo.Invokeon an already-known, non-genericTypeis automatically trim-safe); add[DynamicallyAccessedMembers(...PublicConstructors)]annotations if the real publish-and-run proof surfaces a trim/AOT failure, matching the fix PLAN-0040 Phase 1's real implementation already had to apply. - A dedicated
test/Compono.MSTest.AotSmokeTest(or equivalent) project,dotnet publish -c Release -p:PublishAot=true+ run, exercising the real, packagedCompono.MSTest.ComposeAttribute .GetDatapath directly (not a hand-rolled stand-in for it) — composing both a custom type and a provider-resolved leaf type, through both the no-profile and[Compose<TProfile, TConfig>]forms.-p:TrimmerSingleWarn=falsepass: zero warnings attributable toCompono.MSTest's own shipped code. - Source-level guard (a simple text/syntax scan over
src/Compono.MSTest/**/*.cs, mirroringCompono.Logging's own guard) that fails the build ifMakeGenericType,Delegate.CreateDelegate,DynamicMethod, orSystem.Linq.Expressionsever appears — so a future change can't silently reintroduce a reflection-dispatch path this design rejected.
11. Build/CI infrastructure wiring¶
Creating the projects alone leaves them outside every place this repo's build/release/validation pipeline enumerates packages by name:
-
Compono.slnx— the two core project entries added (task group 1). The sample/AOT-smoke projects (Compono.MSTest.SampleTests,Compono.MSTest.AotSmokeTest) are deliberately not added, matchingCompono.TUnit.SampleTests/Compono.TUnit.AotSmokeTest's own existing precedent — confirmed byCompono.slnx's own comment block: these are manual, one-shot/local-feed-driven proofs run outsidedotnet build Compono.slnx, not part of the ordinary solution-wide build. -
.github/workflows/docs.yml—src/Compono.MSTest/**added to bothpaths:trigger lists,Compono.MSTestadded to the API-reference build loop. -
.github/workflows/package-validation.yaml—Compono.MSTestadded to itsfor pkg in ...loop and explicitpack_one/path lists. -
.github/scripts/inspect-packed-nupkgs.sh—Compono.MSTestadded, with its own expected-dependency-setcasebranch (Compono+MSTest.TestFramework, no embeddedCompono.Generators.dll). -
.github/scripts/generate-api-reference.sh—Compono.MSTestadded tointegration_pkgs. - Confirm, directly (real local pack + restore, not assumed), that a consumer referencing only
Compono.MSTest(pullingComponoin transitively) receivesCompono.Generators.dll's execution with zero extra steps.
12. Documentation (ADR-0057 §16 — completion-gate work, not cleanup)¶
-
docs/packages/compono-mstest.md(new) — followingdocs/packages/compono-tunit.md's shape:[TestMethod]+[Compose]is the intended syntax ([DataTestMethod]unnecessary and actively discouraged,MSTEST0044); the full attribute family with worked examples;[Shared]/Share<T>(); the discovery/execution repeat-composition contract (§9), stated as this package's own documented behavior;[DataRow]/[DynamicData]independent-row boundary (§10); synchronous-only composition; non-ownership/no disposal (§12);TestContextremains MSTest-owned, no auto-injection (§13); MTP and VSTest both supported, MTP modern/preferred but not required (§8);MSTest.TestFramework4.0.0floor and current4.xcompatibility (§7, Amendment 1) — state plainly thatMSTest3.xis not supported and a3.xconsumer must upgrade to4.x, not just omit3.xsilently;GetDisplayName-based seed reporting (§15). -
docs/packages/index.md— addCompono.MSTest's row. -
README.mdanddocs/index.md— addCompono.MSTest's row to both front-door package tables (confirmed by direct read, not assumed —docs/index.mdwas already stale relative toREADME.md, missingCompono.Http/Compono.Loggingtoo; added all three rather than leaving an inconsistent table next to the new one). -
docs/mvp.md— deviation, recorded here rather than silently skipped: confirmed by direct read thatdocs/mvp.mdis a closed historical record of the original MVP milestone (Compono/XunitV3/ NSubstitute/Bogus only) — none ofCompono.TUnit/TestDoubles/DependencyInjection/Http/Loggingwere ever added there either.docs/roadmap/future-packages.mdis the doc actually live-tracking "scoped pre-1.0 package" status (its own "graduated from this page's roadmap" pattern forCompono.TUnit/TestDoubles/Http) — updated that instead:Compono.MSTestremoved from "Admitted candidates", its own graduation paragraph added matching the others' shape, and the "No committed sequence" section's stale two-candidate reference corrected to the one remaining (Compono.NUnit). -
docs/architecture.md/docs/public-api.md— wherever these enumerate supported test frameworks or package guides, add MSTest; verify by direct read whether either currently states an exhaustive/closed framework list that would become stale by omission (docs/public-api.md's "Package Guides" bullet is a known such spot, per its currentCompono.XunitV3,Compono.TUnit, ... enumeration). -
docs/concepts/shared-values.md/docs/getting-started/installation.mdor equivalent how-to pages — extend to nameCompono.MSTestalongside the existing framework packages, following PLAN-0040 Phase 0's own precedent for exactly this kind of stale-doc sweep. - A short "migrating an MSTest
[DynamicData]-based test to[Compose]" migration note — folded intodocs/packages/compono-mstest.md's own "Migrating from[DynamicData]" section rather than a separate top-level file (docs/migrating-from-autofixture.mdis a framework-agnostic, cross-cutting migration; this one is MSTest-specific and belongs with the package guide it's about) — explicitly acknowledges no real internal MSTest consumer existed to validate it against until task group 15's external packaged-consumer validation fixture. - Public-API/reference regeneration (
docs/reference/api) — per ADR-0032, regeneratedocs/reference/api/Compono.MSTest/as part of this PR.
13. Skill/reference synchronization¶
-
skills/compono/SKILL.md— addCompono.MSTestto the package-enumeration sentence; add a.csproj-detection row to the Detection table (<PackageReference Include="Compono.MSTest"→[TestMethod]+[Compose]available, loadreferences/mstest.md); add areferences/mstest.mdrow to the references-index table; removeCompono.MSTestfrom any "don't invent an unshipped package" guardrail's named-absent list, if one exists. -
skills/compono/references/mstest.md(new) — matchingxunit-v3.md/tunit.md's depth:[TestMethod]+[Compose]is the intended syntax;[DataTestMethod]unnecessary;[DataRow]/[DynamicData]do not merge with[Compose]; composition is synchronous; Compono does not own/dispose composed values;TestContextremains MSTest-owned; MTP and VSTest both supported, MTP modern/preferred but not required;GetData/registration factories may run more than once across MSTest discovery/execution sessions and consumers must not rely on exactly-once invocation; reproducibility across separate calls requires an explicitly pinnedSeed— an unpinned[Compose]generates a fresh random seed per call, so a discovery-time row and a later execution-time row generally hold different composed values, not the same ones.
14. Eval coverage¶
-
skills/compono-evals/evals.json— aCompono.MSTest-specific discriminating eval (mirroring RESEARCH-0014'sShare<T>()eval pattern and PLAN-0040's own routing-eval precedent) distinguishing at least one genuine MSTest-specific trap: unnecessary[DataTestMethod]usage, assuming[DataRow]partially merges with[Compose], assuming MTP is required, assumingGetData/factories run exactly once, or trying to auto-composeTestContext. Keep it focused — one or two scenarios proving the skill/reference material actually teaches framework-specific behavior, not a broad testing project. - Run the existing before/after benchmark harness for the new eval(s) against the updated skill, recording the result per
skills/compono-evals' established convention —skills/compono-evals/benchmarks/2026-09-02/README.md(9/9 with skill, 3/9 without; graded by this implementing session directly, same methodology limitation the 2026-08-28 benchmark already records, not a new one).
15. Dedicated external MSTest packaged-consumer validation fixture (separate repo — see Scope's PR-sequencing note; not true product dogfooding — see the fixture's own terminology note below)¶
ADR-0057 §16 requires this as a real consumer-validation target, not an internal-unit-test substitute — no existing LayeredCraft/ncipollina MSTest consumer exists today (checked including branches, per RESEARCH-0017 §17).
Terminology note (2026-09-02 clarification): this fixture is external packaged-consumer validation, not true product dogfooding. Dogfooding implies validating against a real, pre-existing application that actually depends on the package for its own purposes — no such MSTest consumer exists in any LayeredCraft/ncipollina repository, and this task does not manufacture one by migrating a real application to MSTest solely to create that appearance. What this task group does provide, and is real: a dedicated fixture that lives outside Compono.MSTest's own implementation, consumes freshly packed local NuGet packages (never ProjectReference), is validated through scripts/dogfood-validate.sh, exercises realistic Compono.MSTest usage, and validates MTP/VSTest where practical — genuine pre-1.0 external-consumption validation. The distinction matters for the final report: external packaged-consumer validation completed before 1.0 is what this task group delivers; true dogfooding is unavailable because no real MSTest consumer currently exists. If one appears in the future, that can provide genuine dogfooding evidence then — this task group's own fixture is not a substitute for that, and should not be described as if it were.
- Create a small, dedicated, purpose-built MSTest consumer repository (its own
git init, ownDirectory.Packages.props—scripts/dogfood-validate.shrequires a real git repo at--consumer-repowith its ownDirectory.Packages.props, per its existing--consumer-repo/--consumer-solutioncontract; confirm during implementation whether it needs any additive extension beyond pointing--consumer-repo/DOGFOOD_CONSUMER_REPOat the new fixture and--packages/DOGFOOD_PACKAGESat"Compono Compono.MSTest Compono.NSubstitute Compono.Logging"— the script's own--packagesgeneralization (PLAN-0051 task 11) already supports an arbitrary package set, so no script change may be needed; only add one if a real gap surfaces). A single, purpose-built consumer project, not a synthetic one-file example. - Exercise at minimum: ordinary composition; profiles;
[Shared];Share<T>();Register<T>(); constructor selection;Compono.TestDoublesintegration;Compono.Loggingintegration where appropriate; deterministic seed reproduction; diagnostics; the4.0.0version floor; current MSTest4.x; MTP execution; VSTest execution where practical. - Validate via
scripts/dogfood-validate.shagainst freshly packed local packages, neverProjectReferences or stale package artifacts — the same discipline every other package's dogfood validation follows. - Record the fixture's repo location/link and the validation result in this plan's Notes once run for real.
Critical Files¶
src/Compono.MSTest/Compono.MSTest.csproj— newsrc/Compono.MSTest/ComposeAttribute.cs,ComposeAttribute{TProfile}.cs,ComposeAttribute{TProfile,TConfig}.cs,SharedAttribute.cs— newsrc/Compono.MSTest/Binding/BindingPlan.cs,ParameterBindingPlan.cs,PositionalArgumentBinder.cs,ConfigProfileBinder.cs,RowInvokers.cs— new.RowInvokers.csbuilt against coreCompono's existing, unchangedRowInvokerRegistryfrom its first commit; the rest is a package-local port ofCompono.XunitV3's equivalent files (ADR-0057's binding-logic decision).src/Compono.Generators/Discovery/ComposeMethodDiscovery.cs,src/Compono.Generators/ComponoIncrementalGenerator.cs— modified (three new metadata-name constants/registrations forCompono.MSTest's attribute family)test/Compono.MSTest.Tests/*— newtest/Compono.MSTest.SampleTests/*— new (packaged-consumer proof)test/Compono.MSTest.AotSmokeTest/*— newtest/Compono.Generators.Tests/*— modified (new snapshot test forCompono.MSTest-only-reachable discovery)Compono.slnx— modifiedtest/Directory.Build.props— modified (Compono.MSTest.Tests-name exclusion from the xUnit-v3-specificItemGroups)Directory.Packages.props— modified (MSTest.TestFramework/MSTest/Microsoft.NET.Test.Sdk/Compono.MSTestPackageVersionentries).github/workflows/docs.yml,.github/workflows/package-validation.yaml,.github/scripts/inspect-packed-nupkgs.sh,.github/scripts/generate-api-reference.sh— modifieddocs/packages/compono-mstest.md— newdocs/packages/index.md,docs/mvp.md,docs/architecture.md,docs/public-api.md,docs/concepts/shared-values.md,docs/getting-started/installation.md,README.md,docs/index.md,docs/migrating-from-autofixture.md-equivalent migration note — updateddocs/reference/api/Compono.MSTest/— new (regenerated)skills/compono/SKILL.md,skills/compono/references/mstest.md,skills/compono-evals/evals.json— new/updated- A new external MSTest dogfood fixture repository — outside this repo, see task group 15
Test Plan¶
Per testing.md's established pattern: unit coverage for the binding plan and ConfigProfileBinder/seed logic in isolation (test/Compono.MSTest.Tests), generator-discovery snapshot coverage (test/Compono.Generators.Tests), a packaged-consumer end-to-end suite proving the full attribute family through the real Compono.MSTest → Compono dependency chain (test/Compono.MSTest.SampleTests), an API-surface/approval test locking the public shape, Native AOT publish-and-run proof through the real GetData path, and — the items ADR-0057 explicitly needs empirical confirmation for, not just design-time reasoning — a real MTP-vs-VSTest single-run/discovery-then-execution repeat-invocation check (recorded once as a finding, not a permanently asserted test, matching PLAN-0040's own precedent for an equivalent runner-lifecycle property) and a real display-name/seed-visibility check under both runners. Reuses behavioral expectations from Compono.XunitV3.Tests/Compono.TUnit.Tests wherever ADR-0057 states the semantics are intentionally identical, rather than duplicating coverage mechanically. Every task group above carries its own test/verification item — tests land with the behavior they cover, not batched into a later catch-all task group.
Notes¶
MSTest.TestFramework 3.x/4.x binary-incompatibility finding (2026-09-02) — ADR-0057 Amendment 1. While implementing task group 8's compatibility-matrix validation, building src/Compono.MSTest against the originally-accepted 3.0.0 floor and running test/Compono.MSTest.Tests (which resolves MSTest.TestFramework to 4.3.3 transitively via the MSTest meta-package) failed with FileNotFoundException: Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0. Root-caused by direct .nupkg inspection: every 3.x release (including the latest, 3.11.1) compiles its framework types into Microsoft.VisualStudio.TestPlatform.TestFramework.dll; every 4.x release (4.0.0-4.3.3) compiles the same types into a renamed MSTest.TestFramework.dll — two different assembly identities, no type-forwarder/facade bridge in either package. A Compono.MSTest.dll compiled against 3.x cannot implement the ITestDataSource a 4.x test host looks for, and vice versa — this is a hard binary break, not a version-skew warning. Reported to the user rather than silently building around it; the user accepted raising the floor (ADR-0057 Amendment 1: MSTest.TestFramework 4.0.0 minimum, 3.x explicitly unsupported — deliberate product decision for a new pre-1.0 package, not a claim that 3.x has no users) rather than shipping dual binaries/conditional package assets to preserve 3.x. Directory.Packages.props, src/Compono.MSTest/Compono.MSTest.csproj, and this plan's own task groups ⅛/9/12/15 were updated to 4.0.0 throughout. Re-verified after the floor change: src/Compono.MSTest now resolves MSTest.TestFramework to 4.0.0 (its PackageVersion floor) with zero override needed, and the full test/Compono.MSTest.Tests suite (29/29) passes cleanly against it.
Task groups 1-5 implementation (2026-09-02): package/project creation, the full frozen public API (ComposeAttribute/ComposeAttribute<TProfile>/ ComposeAttribute<TProfile, TConfig>/SharedAttribute), the package-local Binding/ port (BindingPlan, ParameterBindingPlan, PositionalArgumentBinder, ConfigProfileBinder, RowInvokers — built against the existing RowInvokerRegistry from the first commit, no throwaway reflection dispatch), the Compono.Generators discovery extension (three new metadata-name registrations, folded into the existing ComposeMethodDiscovery/ComponoIncrementalGenerator pipeline unforked), and GetDisplayName-based seed reporting are implemented and tested. Real findings from this pass: - ITestDataSource.GetData/GetDisplayName are called separately by MSTest with no shared context object threaded between them — unlike Compono.XunitV3's TheoryDataRow.Traits, there is no built-in carrier for a row's seed. Resolved with a ConditionalWeakTable<object?[], object> keyed by the exact row-array instance GetData returned, read back in GetDisplayName — an implementation-time decision RESEARCH-0017 §11/§15 explicitly left open, not specified by the ADR. GetDisplayName_ReportsDifferentSeeds_ForTwoIndependentCalls proves this works correctly across two independent GetData calls with different generated seeds, matching §9's "no state shared across calls" contract. - Full test/Compono.MSTest.Tests suite: 29 tests, covering binding-plan signature validation (generic method, ref/out/in/params, ref struct/pointer, duplicate [Shared], stacked Compose-family attributes), inline-value binding/precedence, [Shared] sharing within one row, negative-seed rejection (both the base form and the pre-config-binding check on ComposeAttribute<TProfile, TConfig>), ComposeAttribute<TProfile>/ ComposeAttribute<TProfile, TConfig> profile application, ConfigProfileBinder failure diagnostics, seed reporting, and the exact four-type public-API-surface lock. All passing. - Generator-discovery snapshot test (MSTestComposeAttributedMethodParameter_GeneratesCompositionPlan) proves a type reachable only through a Compono.MSTest-attributed method's parameter gets both a generated ICompositionPlan<T> and a RowInvokerRegistry registration — the exact gap ADR-0057 §5 requires closing. Full test/Compono.Generators.Tests suite (299/299) confirmed unaffected — this is additive, not a change to existing discovery behavior. - A quick real-run smoke check (not yet the full task-8 matrix): the same 29-test suite passes identically under dotnet test -f net10.0 (MTP, the project's default) and dotnet test -f net10.0 -p:UseVSTest=true. The -p:UseVSTest=true run did not visibly switch off MTP in the console output (EnableMSTestRunner/UseMicrosoftTestingPlatformRunner may be taking precedence) — task group 8 needs to confirm the VSTest adapter path is genuinely exercised, not just that the flag was accepted, before this counts as real VSTest-adapter proof. - Solution-wide dotnet build Compono.slnx: 0 errors, only pre-existing warnings in unrelated projects (Compono.DependencyInjection.Tests xUnit analyzer warnings, present before this work).
Task groups 6–8 implementation (2026-09-02): real-runner verification, not unit-level simulation. Added test/Compono.MSTest.Tests/RealRunnerRowIdentityTests.cs (genuine [TestMethod] + [Compose]/[Shared]/inline-value tests, unlike every other test file in this project, which calls GetData/GetDisplayName directly without going through MSTest's own pipeline) and DataRowCoexistenceTests.cs ([DataRow] + [Compose] stacked on one method). Two new test-only instrumentation hooks on ComposeAttribute (SeedByRowHitCount/MissCount, GetDataCallCount, internal, gated by existing InternalsVisibleTo) — no new public surface, no product-code behavior change.
- Row-array-identity assumption (the seed/display-name bridge's own dependency) — confirmed under both runners.
./Compono.MSTest.Tests --list-tests(MTP discovery) anddotnet vstest Compono.MSTest.Tests.dll -lt//Tests:...(classic VSTest, confirmed via its own distinctVSTest version 18.10.0banner — not just a flag that was silently ignored, the actual concern flagged in the prior round) both show the real, correct per-row seed inGetDisplayName's output, e.g.ComposesTwoStrings_RealRun (Compono, seed: 1913922119), a different seed on every independent run.SeedByRowMissCountstayed0across every scenario tried (ordinary MTP execution, ordinary VSTest execution, MTP discovery, VSTest discovery) — theConditionalWeakTable-keyed bridge's identity assumption holds. Real, non-obvious finding along the way: neither runner callsGetDisplayNameduring ordinary execution (dotnet test/dotnet vstest) — only during discovery/listing.RealRunnerRowIdentityTests' own regression assertion was corrected to reflect this (assertsMissCount == 0unconditionally; does not assertHitCount > 0, since that's execution-vs-discovery-mode-dependent, not a real invariant). - Discovery/execution repeat-composition contract (ADR-0057 §9) — reproduced with current versions, PID-tagged, mirroring RESEARCH-0017 §20/§20a's own methodology. Via a temporary
COMPONO_MSTEST_GETDATA_LOGenv-var-gated logging hook inGetData(removed after capturing evidence — not shipped): - Single combined process (
dotnet test/direct MTP exe invocation, one process doing both discovery and execution): exactly oneGetDatacall per method. Confirmed under MTP. - Single combined process, classic VSTest (
dotnet vstest, one process): exactly oneGetDatacall per method. - Separate discovery process (
--list-tests/-lt) followed by a separate execution process: exactly twoGetDatacalls per method (one per process, confirmed via distinct OS PIDs in the log) — under both MTP and classic VSTest, not only VSTest as RESEARCH-0017's original framing might suggest; the doubling is a property of "discovery and execution are separate process invocations," not specific to the VSTest adapter. This is consistent with, and a same-version reproduction of, RESEARCH-0017's own findings — no contradiction found, no workaround built (no cross-process cache, no static row cache, no customTestMethodAttribute), matching this task group's own constraint. [DataRow]/[Compose]coexistence — confirmed independent-row, not merged, viaDataRowCoexistenceTests.DataRowAndComposeProduceIndependentRows: a method carrying both[DataRow(42, "from-datarow")]and[Compose]produces exactly two test cases under a real run — one with[DataRow]'s exact literal values, one with independently[Compose]-composed values — never a merged/partial row. Passing under both MTP and classic VSTest.- Version-compatibility matrix — all four legs confirmed, floor and current genuinely distinct package versions (
MSTest.TestFramework4.0.0.0vs4.3.3.0— confirmed differentAssemblyVersions by direct inspection, unlike the3.x→4.xboundary's assembly-name break):4.0.0× MTP,4.0.0× classic VSTest (34/34 passing,Compono.MSTestcompiled against its own4.0.0floor, consumed by a test project transitively resolvingMSTest.TestFrameworkto4.3.3via theMSTest4.3.3 meta-package — i.e. the realistic "package built once, consumed against a newer same-major version" shape);4.3.3× MTP,4.3.3× classic VSTest (34/34 passing,Compono.MSTestitself temporarily rebuilt against4.3.3viaVersionOverride, reverted immediately after — the shipped package still targets the4.0.0floor). Within the4.xline, floor and current are ABI-compatible (.NET's ordinary same-name assembly version tolerance) — this is exactly the property the3.x→4.xboundary lacked.
Task groups 4 (remaining item), 9-15 implementation (2026-09-02):
- Task group 9 — packaged-consumer sample project. New
test/Compono.MSTest.SampleTests(mirrorsCompono.TUnit.SampleTests' ownpack-to-local-feed.sh/isolated-RestorePackagesPathshape exactly):CompositionTests.cs,SharedTests.cs,ConfigProfileTests.cs,NSubstituteTests.cs— the complete attribute family ([Compose]/[Compose<TProfile>]/[Compose<TProfile, TConfig>]/[Shared]) through the real packagedCompono.MSTest→Compono→Compono.Generatorsdependency chain, never aProjectReference. 5/5 passing under bothdotnet test(MTP, all 4 TFMs in Release) anddotnet vstest(classic adapter, confirmed via its own distinct "VSTest version 18.10.0" banner). This also closed task group 4's own outstanding "minimal local-feed packaged-consumer proof" item — it now has the fuller proof rather than a separate minimal one. - Task group 10 — Native AOT. New
test/Compono.MSTest.AotSmokeTest(mirrorsCompono.TUnit.AotSmokeTest'spack-compono.sh/local-feed shape), driving the real, packagedComposeAttribute.GetData(MethodInfo)directly (simpler thanCompono.TUnit's own harness — MSTest hands a plainMethodInfo, no hand-builtDataGeneratorMetadataneeded). Real trim gap found and fixed, identical in shape toCompono.TUnit's own ADR-0041 Amendment 1 finding:ConfigProfileBinder'sConstructorInfo.Invoke-basedTConfig/TProfileconstruction failed at runtime underdotnet publish -p:PublishAot=truewith'ProfileConfig' must have exactly one public constructor ... but has 0— the trimmer strips a closed generic argument's public constructors by default. Fixed with[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]annotations onConfigProfileBinder'sType-typed parameters and onComposeAttribute<TProfile, TConfig>'s ownTProfile/TConfigtype parameters. Re-verified clean: both[Compose]and[Compose<TProfile, TConfig>]pass under a realdotnet publish -c Release -f net10.0 -p:PublishAot=true -r osx-arm64 --self-contained true+ run, and a-p:TrimmerSingleWarn=falsepass shows zero warnings attributable toCompono.MSTest's own code. A source-level guard (test/Compono.MSTest.Tests/ReflectionSourceGuardTests.cs) scanssrc/Compono.MSTest/**/*.csforMakeGenericType/MakeGenericMethod/Activator.CreateInstance/DynamicMethod/Delegate.CreateDelegate/System.Linq.Expressions— none found (confirmed by direct grep before writing the test). - Task group 11 — CI/packaging wiring.
docs.yml,package-validation.yaml,inspect-packed-nupkgs.sh,generate-api-reference.shall updated and exercised for real, not just edited: a freshdotnet packof all nine publishable packages run throughinspect-packed-nupkgs.shpassed every assertion forCompono.MSTest(exact file listing — confirming no embeddedCompono.Generators.dll, correct<title>, exact-pinComponodependency,MSTest.TestFramework[4.0.0, 5.0.0)range); a realdotnet build -p:WarningsAsErrors=CS1591pass onCompono.MSTest.csprojwas clean (every public member documented);generate-api-reference.shwas actually run, producingdocs/reference/api/Compono.MSTest/with git-diff-confirmed zero drift anywhere else.Compono.MSTest.SampleTests/Compono.MSTest.AotSmokeTestdeliberately not added toCompono.slnx, matchingCompono.TUnit.SampleTests/Compono.TUnit.AotSmokeTest's own established, documented convention (manual/local-feed-driven proofs, not part ofdotnet build Compono.slnx). - Task groups 12-14 — documentation/skill/eval. New
docs/packages/compono-mstest.md(full package guide, including the[DynamicData]-migration section task group 12 required).docs/roadmap/future-packages.md— real deviation from the plan's original task wording, recorded rather than silently substituted:docs/mvp.mdturned out to be a closed historical record of the original MVP milestone only (confirmed by direct read — none ofCompono.TUnit/TestDoubles/DependencyInjection/Http/Loggingwere ever added there either);docs/roadmap/future-packages.mdis the doc that actually live-tracks "scoped pre-1.0 package" status via its own established "graduated from this page's roadmap" pattern, soCompono.MSTestwas graduated there instead, matching precedent exactly.README.md/docs/index.md/docs/packages/index.md/docs/public-api.md/docs/concepts/shared-values.md/docs/getting-started/installation.mdall updated (founddocs/index.mdalready stale relative toREADME.md, missingCompono.Http/Compono.Loggingtoo — fixed alongside the newCompono.MSTestrow rather than leaving an inconsistent table next to it).skills/compono/SKILL.md(package enumeration, Detection table, named-absent guardrail list, references index) and newskills/compono/references/mstest.md(matchingtunit.md's depth). Two new evals (ids 40-41 inskills/compono-evals/evals.json) — a routing scenario and a three-part behavioral-correctness scenario ([DataTestMethod]unnecessary,[DataRow]/[Compose]never merge, no exactly-onceGetDataguarantee). Benchmark recorded inskills/compono-evals/benchmarks/2026-09-02/README.md(9/9 with skill, 3/9 estimated without — graded by this implementing session directly, same methodology limitation the 2026-08-28 benchmark already records). - Task group 15 — external MSTest packaged-consumer validation fixture (terminology per the 2026-09-02 mid-turn clarification: this is not true product dogfooding — no real LayeredCraft/ncipollina MSTest consumer exists, and none was manufactured to create that appearance). Created a small, purpose-built fixture (
OrderProcessing.Tests, its owngit init/Directory.Packages.props/.gitignore) at a location external to this repository, exercising ordinary composition,[Compose<TProfile>]withUseNSubstitute()+[Shared],Register<T>(),Share<T>()graph-wide sharing, constructor selection (a private constructor correctly excluded as a candidate),Compono.TestDoublesintegration (UseGeneratedTestDoubles()+Verify()),Compono.Loggingintegration (UseLogging()+GetLastCapturedEntry()), deterministic seed reproduction, and a negative-seedCompositionExceptiondiagnostic. Validated viascripts/dogfood-validate.sh— no script changes were needed, confirming the plan's own prediction that the script's existing--packagesgeneralization (PLAN-0051 task 11) would be sufficient.
Reproducible fixture spec (recorded in full here, not by reference to the ephemeral local checkout, per the 2026-09-02 adversarial-review finding that a <fixture> placeholder isn't durable evidence):
- Layout: a standalone git repository (
git init, no relation to this repo's history), root filesOrderProcessing.slnx(one project,tests/OrderProcessing.Tests/OrderProcessing.Tests.csproj),Directory.Packages.props,.gitignore(bin/,obj/). Test project files:Domain.cs(Customer,Order,IOrderRepository,OrderService,INotifier,OrderNotifier,ShippingAddress— the last with one public + one private constructor),CompositionTests.cs,TestDoublesIntegrationTests.cs,LoggingIntegrationTests.cs. Directory.Packages.props(ManagePackageVersionsCentrally=true): placeholderPackageVersionentries forCompono/Compono.MSTest/Compono.NSubstitute/Compono.TestDoubles/Compono.Loggingat0.0.0each (the versiondogfood-validate.shoverwrites via its own generated temp copy — the placeholder value itself is never restored against), plusMSTest4.3.3andMicrosoft.Extensions.Logging.Abstractions10.0.10pinned directly (not swapped by the script).- Test project csproj:
TargetFramework net10.0,EnableMSTestRunner/TestingPlatformDotnetTestSupport/UseMicrosoftTestingPlatformRunneralltrue,ComponoGeneratedTestDoubles=true.PackageReferenceonly — zeroProjectReferenceto anysrc/Compono*project anywhere in the fixture, confirmed by direct read of the one csproj file (MSTest,Compono.MSTest,Compono.NSubstitute,Compono.TestDoubles,Compono.Logging). - Exact command run:
bash scripts/dogfood-validate.sh --consumer-repo <path-to-fixture> --consumer-solution <path-to-fixture>/OrderProcessing.slnx --packages "Compono Compono.MSTest Compono.NSubstitute Compono.TestDoubles Compono.Logging" --feed-dir <throwaway-feed-dir>(from this repo's root). - Package versions actually validated: all five requested packages packed and resolved at the identical freshly-built local version
99.0.0-local.20260902110005-89031-26434(confirmed bydogfood-validate.sh's own built-in anti-stale-cache assertion, which greps every restoredproject.assets.jsonand fails the run if any requested package resolves to anything else);MSTest/MSTest.TestFramework/MSTest.TestAdapter/MSTest.Analyzersat4.3.3(the pinned, non-swapped entry). - Result:
dogfood-validate.sh: PASS - consumer test suite succeeded against local Compono 99.0.0-local.20260902110005-89031-26434— 9/9 tests, exit code 0. Fixture repo's git working tree confirmed byte-identical before/after (the script's own safety-net status/diff comparison, both empty). - Runner paths exercised: the script's own
dotnet testinvocation (MTP) — 9/9; separately,dotnet vstest <built-consumer-dll>(classic VSTest adapter, confirmed via its own distinct "VSTest version 18.10.0" banner, not inferred from a flag) — 9/9 again, same assembly. - Cleanup: the fixture and its throwaway local NuGet feed directory were deleted after validation — this is a disposable, reproducible- from-spec proof artifact, not a retained system, per explicit direction not to preserve a fake long-lived external repo merely to keep a URL. Anyone needing to re-verify this can reconstruct the exact fixture from the spec above and rerun the exact command.
Two real, non-Compono-defect issues found and fixed while building the fixture itself, both instructive: (1) an XML comment containing -- broke MSBuild's parser entirely (Directory.Packages.props silently failed to import, surfacing as a confusing downstream NU1015/NU1101 rather than the real MSB4024 XML error — worth remembering for any future hand-authored .props/.targets file); (2) a deliberately ambiguous two-public-constructor type correctly triggered CMP0001 ("has 2 accessible constructors and no way to disambiguate them") rather than Compono silently picking one — this is Compono's real, intentional behavior, not a bug; the fixture's own Customer type was simplified to one constructor, and ShippingAddress (one public, one private constructor) was added to exercise real, unambiguous constructor selection instead.
Adversarial review response (2026-09-02): an independent review ("Pi") of the above found one HIGH and three MEDIUM issues. All four addressed for real, not just noted:
- HIGH — the
4.0.0/4.xcompatibility matrix's "4.0.0" legs hadn't actually run against 4.0.0. Confirmed correct: the original two "4.0.0" legs consumedCompono.MSTest(compiled against its own4.0.0floor) from a test project that transitively resolvedMSTest.TestFrameworkto4.3.3via theMSTest4.3.3meta-package — real evidence for "floor-compiled binary runs under current," not for "the floor version itself executes." Fixed by re-running with the wholeMSTestfamily genuinely pinned to4.0.0: temporarily addedVersionOverride="4.0.0"to theMSTestPackageReferencein bothtest/Compono.MSTest.Tests.csprojandtest/Compono.MSTest.SampleTests.csproj, restored, and confirmed by directproject.assets.jsoninspection thatMSTest/MSTest.TestFramework/MSTest.TestAdapter/MSTest.Analyzersall resolved to exactly4.0.0(not just the requested version — the actual resolved one). Ran all four real legs: Compono.MSTest.Tests,MSTestfamily4.0.0, MTP (dotnet test -f net10.0): 35/35 passing.Compono.MSTest.Tests,MSTestfamily4.0.0, classic VSTest (dotnet vstest <dll>): 35/35 passing.Compono.MSTest.SampleTests(the real packaged-consumer project, not just the unit suite),MSTestfamily4.0.0, MTP: 5/5 passing.Compono.MSTest.SampleTests,MSTestfamily4.0.0, classic VSTest: 5/5 passing.
Current 4.x (4.3.3) × MTP/VSTest was already proven for real earlier in this Notes section (both the unit suite and the sample project). The VersionOverrides were reverted immediately after capturing this evidence — the shipped csproj files still target the plain 4.0.0 floor via the central [4.0.0, 5.0.0) range, confirmed by re-restoring and re-running (35/35) after the revert. No dependency conflict was hit — a genuine 4.0.0 leg was achievable and is now proven, not just claimed. - MEDIUM — GetDisplayName/dotnet test wording overclaimed. Confirmed real by the implementation evidence already in this Notes section (the RealRunnerRowIdentityTests hit/miss-counter finding). Per explicit direction, treated as an ADR-level correction, not just a doc edit: added ADR-0057 Amendment 2 (GetDisplayName is a discovery/listing-time surface — --list-tests, dotnet vstest -lt, Test Explorer's tree population — under both MTP and classic VSTest, and is not called during an ordinary dotnet test/dotnet vstest execution run under either runner; a composition failure's seed is independently carried by CompositionException.WithSeedInMessage regardless of GetDisplayName). docs/packages/compono-mstest.md's "Seed and display name" section and skills/compono/references/mstest.md's equivalent bullet were both rewritten to state the corrected, verified behavior directly, distinguishing discovery/listing display names from execution-time CompositionException diagnostics. The ConditionalWeakTable implementation itself was not changed — per explicit direction, this finding is about when MSTest calls the hook, not a defect in the bridge. - MEDIUM — [DynamicData] coexistence was claimed but never actually exercised. Confirmed: only [DataRow] had a real runner-level test. Fixed by extending DataRowCoexistenceTests.cs with a genuine [DynamicData(nameof(GetDynamicRows))] + [Compose] stacked scenario (DynamicDataAndComposeProduceIndependentRows), proving the identical independent-complete-row behavior for [DynamicData] specifically, not inferred from the [DataRow] case. No merging machinery was added. 37/37 passing (up from 35) under both MTP and classic VSTest. - MEDIUM — external validation evidence wasn't durable. Addressed directly above (this Notes entry now records the full fixture spec, exact command, exact resolved versions, and exact result — reproducible without the now-deleted fixture existing), per explicit direction not to fabricate a permanent fake external repo. - The Compono.XunitV3.Binding.ConfigProfileBinder Native AOT finding (identified by the review as a likely pre-existing latent gap) is recorded as ADR-0041 Amendment 4 — found incidentally while fixing Compono.MSTest's own identical gap, explicitly not fixed as part of ADR-0057/PLAN-0057, and explicitly not applied to any Compono.XunitV3 source file. Tracked as a separate future follow-up against ADR-0041, per direction. - Not touched, per explicit direction — no new contradictory evidence surfaced: the ConditionalWeakTable design, the Compono.MSTest DynamicallyAccessedMembers fix, and the public API surface all stand as already accepted.
Final validation gate, re-run after the adversarial-review fixes (2026-09-02): dotnet build Compono.slnx — 0 errors, 0 new warnings. dotnet test Compono.slnx -f net10.0 — whole-repo run, 964/964 passing, zero regressions in any existing package (Compono.Tests 291, Compono.Generators.Tests 299, Compono.XunitV3.Tests 76, Compono.TUnit.Tests 52, Compono.NSubstitute.Tests 23, Compono.Bogus.Tests 63, Compono.TestDoubles.Tests 6, Compono.DependencyInjection.Tests 17, Compono.Http.Tests 29, Compono.Logging.Tests 59, samples 12, plus Compono.MSTest.Tests' own 37 — up from 35, the two new [DynamicData] rows). Packaging (inspect-packed-nupkgs.sh), CS1591 doc-completeness, API reference regeneration, Native AOT (both attribute forms, zero Compono.MSTest-attributable trim warnings), MTP + classic VSTest (unit suite, sample project, and the external validation fixture, all three, now including a genuine MSTest 4.0.0-family leg for both the unit suite and the sample project, not only current 4.3.3), the full 4.0.0/current-4.x × MTP/VSTest compatibility matrix (actual resolved package versions confirmed by direct project.assets.json inspection, not requested/configured versions), and [DataRow]/[DynamicData] coexistence (both, not just [DataRow]) were each verified for real, not assumed — see the task-group entries and the adversarial-review-response entry above for each one's own command/output. Working tree: every change is an intentional part of this feature (git status reviewed, all VersionOverride experiments reverted and re-verified passing at the shipped 4.0.0 floor); the external validation fixture and its throwaway local NuGet feed directories lived entirely outside this repository and were deleted after validation — this Notes section's own fixture-spec entry is the durable, reproducible record of what was validated.
PLAN-0057 is now legitimately Done — all 15 task groups' checkboxes are checked, and the four Pi-identified findings (one HIGH, three MEDIUM) are resolved with real, re-verified evidence rather than narrowed claims. Not committed, pushed, or opened as a PR, per this session's explicit instructions throughout. Status below reflects the work done; the user should review before this plan is considered fully closed out in the sense of a merged PR.