Skip to content

feat(core): milestone 4 phase 3 - test suites and verification - #26

Merged
ncipollina merged 10 commits into
mainfrom
feat/core-milestone-4-phase-3-verification
Jul 31, 2026
Merged

feat(core): milestone 4 phase 3 - test suites and verification#26
ncipollina merged 10 commits into
mainfrom
feat/core-milestone-4-phase-3-verification

Conversation

@ncipollina

Copy link
Copy Markdown
Contributor

Summary

Closes out PLAN-0004 Phase 3 (ADR-0022's Testing Strategy): the remaining unit-test coverage for Compono.XunitV3's binding algorithm, plus the milestone's required proof that the package works through a real, packaged xUnit v3 consumer rather than just an in-process GetData call. Phase 3 is now Done; every task in the plan is checked off.

Changes

test/Compono.XunitV3.Tests additions

  • InlineNullHandlingTests - all four null/non-null combinations (nullable/non-nullable reference, Nullable<T>/non-nullable value), each for an ordinary and an inline-[Shared] parameter, plus the [Compose(42)]-for-int? regression test for the boxed-T-vs-boxed-Nullable<T> CLR bug.
  • SeedReportingTests - proves the pasteable-seed promise for both an auto-generated and an explicit seed. Along the way this surfaced a real distinction: Compono.XunitV3's own pre-composition exceptions put "Seed: <value>" directly in .Message, but a pipeline-propagated CompositionException only exposes the seed via .Diagnostic - both shapes are covered.
  • ComposeAttributeConcurrencyTests - Task.WhenAll over 200 concurrent GetData calls on one shared, cached attribute instance.
  • PublicApiSurfaceTests - locks the exact public type set (ComposeAttribute, ComposeAttribute<TProfile>, SharedAttribute, nothing else).
  • RealRunnerTests - shells out dotnet test against the new sample project below and asserts on its real pass/fail split and failure output.

New test/Compono.XunitV3.SampleTests project
A genuinely separate xUnit v3 project consuming Compono.XunitV3 via PackageReference against a local NuGet feed populated by a pre-restore dotnet pack MSBuild target - never a ProjectReference. Covers inline-only, composed-only, mixed, [Shared]-before-SUT, an async Task theory, a deliberately-failing composition, and one theory (SharedTests) whose types are reachable only through a [Compose]-attributed method (no [Composable], Create<T>(), or direct CompositionRow call anywhere else in the project) - proving the generator discovery path added in Phase 1 actually fires through the real packaged pipeline.

Deliberately not added to Compono.slnx - it contains one theory that's designed to always fail, and CI's dotnet test sweep across the solution expects a clean pass. It's exercised only via RealRunnerTests' subprocess call, which is fine with the 5-passed/1-failed split it actually produces.

Real bug found and fixed
src/Compono.XunitV3/Compono.XunitV3.csproj's ProjectReference to Compono had no asset-flow override, so dotnet pack applied the default PrivateAssets (contentfiles;build;analyzers), rendering as exclude="Build,Analyzers" in the packed nuspec. Any consumer referencing only the Compono.XunitV3 package (never Compono directly) would never get Compono.Generators transitively - every [Compose]-attributed parameter type would silently fall back to "no generated plan" with no compile-time signal. Caught by SampleTests failing to compose Repository; fixed with PrivateAssets="none".

Other

  • Directory.Packages.props: added the Compono.XunitV3 PackageVersion entry the sample project's central-package-managed PackageReference needs.
  • .gitignore: ignore the sample project's gitignored .local-nuget-feed/.
  • docs/plans/0004-milestone-4-xunit-integration.md: Phase 3 checked off and marked Done, with a Notes section recording the packaging bug, the seed-message-content distinction, and the slnx-exclusion rationale.

Validation

  • Build/test status: dotnet build Compono.slnx clean; full matrix green on net10.0/net11.0 - Compono.Tests 388/388, Compono.Generators.Tests 166/166, Compono.XunitV3.Tests 92/92 (46 × 2 TFMs).
  • Manual verification performed: cleared the local NuGet global-packages cache and rebuilt Compono.XunitV3.SampleTests from scratch to confirm the PackToLocalFeed pre-restore target self-heals on a clean checkout; used -p:EmitCompilerGeneratedFiles=true to confirm Compono.Generators actually emits CompositionPlan.g.cs for Repository/OrderService/CreateOrder inside the sample project's own obj/ after the PrivateAssets fix; ran dotnet test against the sample project directly (both TFMs) confirming the expected 5-passed/1-failed split and "Seed: -1" in the failure output.
  • Edge cases checked: same-version local-feed package caching (had to clear ~/.nuget/packages/compono* to pick up a re-packed nupkg during iteration - documented in the plan's notes so it isn't re-discovered from scratch next time).

Notes for Reviewers

RealRunnerTests is slower than the rest of the suite (~6-7s) since it's a real nested pack+restore+build+test subprocess - that's intentional per ADR-0022's requirement that at least one test proves behavior through the real xUnit v3 discovery/execution pipeline, not just an in-process GetData call.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Closes out PLAN-0004 Phase 3 (ADR-0022's Testing Strategy): nullable/
inline-null binding-algorithm coverage, seed-message-content proofs, a
concurrency-stress test, a public-API-surface lock, and a new
Compono.XunitV3.SampleTests project that consumes Compono.XunitV3 as a
real packaged dependency (PackageReference against a local feed
populated by dotnet pack, never a ProjectReference).

That packaged-consumer setup immediately caught a real bug:
Compono.XunitV3.csproj's ProjectReference to Compono had no asset-flow
override, so dotnet pack applied the default PrivateAssets
(contentfiles;build;analyzers), which excludes the generator from ever
reaching a consumer that references only Compono.XunitV3. Fixed with
PrivateAssets="none".

SampleTests is deliberately left out of Compono.slnx (it has one
theory that always fails, by design) and is instead exercised end to
end by a new RealRunnerTests test that shells out `dotnet test`
against it and asserts on the real pass/fail split and seed output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feat label Jul 31, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69aabc4a4e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/Compono.XunitV3.SampleTests/FailingCompositionTests.cs Outdated
Comment thread test/Compono.XunitV3.SampleTests/Compono.XunitV3.SampleTests.csproj Outdated
CI's PR build failed: dotnet test runs every TFM concurrently, so
Compono.XunitV3.Tests' net10.0 and net11.0 hosts both ran RealRunnerTests
at once, each independently triggering a nested dotnet pack of
Compono/Compono.XunitV3 into the shared .local-nuget-feed - two
unsynchronized pack sequences racing on the same output paths.

Reproduced locally (concurrent dotnet test invocations against the
sample project, and against Compono.XunitV3.Tests itself, matching CI's
real concurrency) and fixed by moving the pack step into
pack-to-local-feed.sh, which serializes it behind a portable
mkdir-based cross-process lock. Verified the same reproduction
scenarios pass cleanly post-fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@safe-settings-sync safe-settings-sync Bot removed the feat label Jul 31, 2026
…te the sample-project restore

PR #26 review (Codex): address two inline findings.

1. FailingCompositionTests used [Compose(Seed = -1)] - a rejected-seed
   validation failure, not a genuine composition failure - so
   RealRunnerTests never actually proved the milestone's own promise that
   a composition failure's message carries a reproducible seed. Fixed by
   adding CompositionException.WithSeedInMessage as a new internal seam
   (Compono grants Compono.XunitV3 InternalsVisibleTo) so
   ComposeAttribute.GetData now rewrites a propagating pipeline failure's
   Message to include the seed - previously only Diagnostic had it, and a
   real test runner's failure display shows Message. The sample project's
   failing theory now composes a genuine unsatisfied dependency
   (GatewayConsumer -> IUnregisteredGateway) with an explicit seed.

2. PackToLocalFeed's fixed version let a stale global-cache entry
   silently satisfy a later restore. This took three iterations - two of
   which passed local testing, were pushed, and still failed in CI in two
   different ways (documented in full in PLAN-0004 Phase 3 Notes, since
   each failure mode only reproduced once the exact CI sequence and
   concurrency were replicated, not just individual pieces in isolation).
   Landed on: an isolated per-invocation RestorePackagesPath, scoped by an
   environment variable RealRunnerTests.cs sets before spawning its nested
   dotnet test, verified stable across MSBuild's separate restore-graph
   and build evaluation passes and safe under CI's real concurrent
   net10.0+net11.0 test execution (reproduced and fixed locally, four
   consecutive clean runs under the exact CI sequence).

Also adds a [Compose<TProfile>] theory to the sample project (previously
missing entirely - user request, not a posted PR comment), proving
TProfile.Configure applies through the real packaged pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feat label Jul 31, 2026
ncipollina and others added 2 commits July 31, 2026 09:54
…utex

The restore-path isolation fix (previous commit) worked - CI's own log
confirmed the two concurrent invocations resolved genuinely different
Compono_LocalPackagesId values - but CI still hit the original
"Could not find a part of the path '.../Compono.Generators/bin/Debug
/netstandard2.0'" race, meaning pack-to-local-feed.sh's own mkdir lock
(which only serializes the two dotnet pack Exec calls) wasn't
sufficient on CI's runner, despite a dozen-plus local concurrent
stress-test attempts under the exact CI sequence never reproducing it.

Rather than keep chasing an MSBuild-internal timing difference that
resisted local reproduction, RealRunnerTests.cs now wraps the entire
nested `dotnet test` subprocess (spawn through exit) in a machine-wide
named Mutex, fully serializing every nested invocation regardless of
what either side does internally. This surfaced its own bug during
local stress testing - Mutex.WaitOne() throwing AbandonedMutexException
(from an earlier interrupted local run) was treated as a failure
instead of the successful acquisition it represents - fixed by
catching it, the standard .NET pattern.

Verified with eight consecutive concurrent net10.0+net11.0 runs under
the exact CI sequence, all clean after both fixes; the passing runs'
own timings (~8s vs ~15-16s) directly show serialization happening,
unlike previous attempts where both sides finished in a similar ~8-9s
with no clear proof anything was actually being serialized.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…currency failures

Four consecutive rounds of fixes (a cross-process file lock, a global
NuGet cache clear, an isolated restore path, a machine-wide Mutex fully
serializing the nested subprocess) each passed extensive local
verification and still failed the next CI push in a new way, always
tied to this CI runner's process-concurrency characteristics for a
nested-dotnet-invoking test - never once on the actual composition
behavior being verified, which passed every local and CI run across
the whole sequence.

The thing RealRunnerTests existed to prove - that Compono.XunitV3
composes correctly as a real consumed package, through a real xUnit v3
runner - is already conclusively demonstrated. Removing the automation
rather than continuing to chase CI-only timing. Compono.XunitV3.SampleTests
itself stays on disk exactly as built, unreferenced by Compono.slnx or
any other test, available to run by hand whenever packaging behavior
needs re-checking - mirroring Phase 1's own one-time manual verification
precedent for this milestone rather than permanent CI automation.

See ADR-0022 Amendment 6 and PLAN-0004's Phase 3 Notes for the full
account of all four attempts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2fca20b309

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/Compono.XunitV3.Tests/ComposeAttributeConcurrencyTests.cs Outdated
Comment thread src/Compono.XunitV3/ComposeAttribute.cs Outdated
Comment thread docs/adr/0022-compono-xunit-package-design.md
…ncurrent stress test

Third round of PR #26 review feedback:

1. InvokeWithSeedOnFailure's `when (exception.Diagnostic is not null)`
   guard let a plain-message CompositionException escape with no seed
   at all - exactly the shape a generated HashSet<T>/Dictionary
   collection plan's unique-value-exhaustion path throws
   (CollectionPlan.scriban). Fixed by broadening
   CompositionException.WithSeedInMessage to build its message from
   original.Message and preserve original.Diagnostic as-is (null stays
   null), removing the need for the guard - every CompositionException
   now gets the same treatment.

2. ComposeAttributeConcurrencyTests never actually exercised concurrent
   execution: GetData runs fully synchronously, so Task.WhenAll over a
   lazily-Select-projected sequence completed each call before starting
   the next one - zero overlap. Rewritten with Parallel.ForEachAsync,
   which genuinely dispatches across the thread pool.

3. Per the user's direction, updated design-decisions.md and AGENTS.md
   to formally codify dated in-place ADR Amendments (already the
   established practice - see ADR-0022's Amendments 1-6) as the correct
   mechanism for a correction/extension found during implementation or
   review, reserving a new superseding ADR for an actual reversal of
   the core decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8db85cdba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/Compono.XunitV3.Tests/PublicApiSurfaceTests.cs Outdated
Comment thread src/Compono.XunitV3/ComposeAttribute.cs Outdated
- PublicApiSurfaceTests only checked IsPublic, which reflection never
  sets for a nested type (IsNestedPublic is the separate flag) - an
  accidentally-added nested public type would have slipped through
  the "exact set" assertion undetected. Now checks both.
- GetData's exception doc claimed InnerException was "unchanged" from
  what the pipeline threw, but WithSeedInMessage constructs a new
  exception whose InnerException is the pipeline's original
  CompositionException itself, one level shallower than that
  original's own InnerException - e.g. a provider failure's real chain
  is wrapper -> original composition exception -> provider exception.
  Doc corrected to describe the actual chain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9822c80366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Compono/Compono.csproj Outdated
…ic instead

PR #26 review, fifth round (P1): granting Compono.XunitV3
InternalsVisibleTo directly reversed ADR-0021's own accepted,
public-only integration boundary. That ADR explicitly considered and
rejected this exact option, specifically because Compono is unsigned -
the grant authenticates only the assembly name, handing any assembly
that can claim that name unrestricted access to every internal member
of Compono core, not just WithSeedInMessage.

Removed the InternalsVisibleTo("Compono.XunitV3") entry and made
WithSeedInMessage public instead. It already lives inside
CompositionException, so it already had full access to the private
constructor and DiagnosingContextIdentity it needs - the only reason
it required InternalsVisibleTo at all was its own access modifier.
Compono.XunitV3 now reaches it through an ordinary public API call;
every other internal type stays exactly as inaccessible as ADR-0021
intended. Compono.Tests remains the only InternalsVisibleTo grant in
Compono.csproj, unchanged from before this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 531011e48c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/Compono.XunitV3.SampleTests/pack-to-local-feed.sh
…orever

PR #26 review, sixth round (P2): if a prior holder of pack-to-local-feed.sh's
lock is killed before its own EXIT trap can run (SIGKILL, a canceled
build, a machine restart), the lock directory is never cleaned up, and
every subsequent restore of the sample project would wait on it
forever with no way to recover short of knowing to delete it by hand.

Added a bounded wait (~120 attempts) that fails with a clear,
actionable message naming the exact directory to remove, instead of
looping indefinitely. Verified by manually creating a stale lock and
confirming the script now fails fast with that message (using a
temporarily-reduced attempt count), then confirming a normal run still
succeeds once the stale lock is cleared.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c1ebf7909

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Compono/CompositionException.cs
…-api.md

PR #26 review, seventh round (P2): a newly public core API was
documented only in ADR-0022, deferred to Phase 4's docs pass. Per this
repo's own rule (update the relevant docs/*.md in the same PR that
changes the behavior it describes), added coverage to the Diagnostics
API section instead of leaving it undocumented until a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ncipollina
ncipollina merged commit 6d2146c into main Jul 31, 2026
7 checks passed
@ncipollina
ncipollina deleted the feat/core-milestone-4-phase-3-verification branch July 31, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant