Consumer Contract Tests answer a simple question: if a customer cannot see the product repository and receives only a template plus private-feed access, can the project still build and publish?
Why integration tests are not enough
Product Hosts can reference projects directly and may see local NuGet caches. That can hide missing package dependencies, Generators that are only accidentally visible, and templates that reference or copy product source.
The Consumer Contract removes those conveniences and verifies again.
Local contract flow
Read commercial catalog → pack candidates into a temporary feed → create an external Consumer project → write NuGet.Config with <clear /> and Package Source Mapping → isolate NuGet, HTTP, plugin, and CLI caches → restore / build / test / Release publish → inspect package assets and source exposureRun it with:
dotnet test tests/BitzOrcas.Framework.ConsumerContract.Tests \ --configuration Release \ --blame-hang-timeout 12mRequired assertions
- no product-repository
ProjectReference; - no copied Framework, Platform, or Licensing implementation source;
- every
BitzOrcas.*package comes from the mapped feed; - Profile closure is complete and version-consistent;
- Generators arrive as analyzer assets in the correct Host;
- build, test, and Release publish pass;
- customer modules reference only allowed Contracts/SDK seams.
Typical failures
| Symptom | Likely cause | Check first |
|---|---|---|
NU1101 | Package not packed, catalog omission, or source mapping mismatch | Temporary feed, catalog, NuGet.Config |
| Generated extension missing | Generator not projected into the Host | Profile generator/hostRoles mapping |
| Local pass, CI fail | Global cache leaked into the test | Isolation variables and cache directories |
| Publish failure | Runtime assets, RID, or incorrect dependencies | First publish error and dependency graph |
| Product source found | Template copied a prohibited directory | Template content and exposure assertions |
Difference from formal GA
The local contract repacks current source for PR and development feedback. Formal GA must use immutable release artifacts, an authenticated HTTPS feed, signatures, provenance, SBOM, and policy evidence.
Current coverage matrix
| Dimension | Local Consumer Contract | Formal Commercial GA |
|---|---|---|
| Package source | Pack current source into a temporary folder feed | Immutable nupkg files from release workflow |
| Consumer location | System temporary directory | Repository-external Solution in release artifact |
| Feed | Isolated local feed + source mapping | Authenticated HTTPS feed + short-lived read credential |
| Cache | Isolated NuGet/HTTP/plugin/CLI state | Same empty-cache discipline |
| Signature | Not a local-contract conclusion | dotnet nuget verify --all + fingerprint set |
| Provenance/SBOM | Does not validate formal evidence | Schema, hash, policy, and exemption-expiry checks |
| Publish | mini-api: PublishTrimmed=false; default-business: adds a Mediator trim publish | Release publish; project settings determine trimming |
Why isolation covers four caches
Clearing only NUGET_PACKAGES is insufficient. The harness isolates:
NUGET_PACKAGES, the global package cache;NUGET_HTTP_CACHE_PATH, HTTP metadata and responses;NUGET_PLUGINS_CACHE_PATH, Credential Provider/plugin state;DOTNET_CLI_HOME, first-run, template, and user-level CLI state.
NuGet.Config starts with <clear /> and maps BitzOrcas.* to the isolated feed, preventing accidental restore from nuget.org or an undeclared machine source.
# Run the full contract set; the fixture packs once and tests copy its read-only baseline.dotnet test tests/BitzOrcas.Framework.ConsumerContract.Tests \ --configuration Release \ --blame-hang-timeout 12m \ --logger 'console;verbosity=normal'
# Isolate the Mini API external-consumption happy path.dotnet test tests/BitzOrcas.Framework.ConsumerContract.Tests \ --configuration Release \ --filter 'FullyQualifiedName~Profile_Mini_Api_Should_Restore_Build_And_Publish'What deletion tests prove
A positive restore can be a false positive if the Profile meta-package is empty and the Consumer never truly depends on it. The contract copies the feed, deletes a Profile package or transitive closure dependency, and requires empty-cache restore to fail.
Deletion proves the package carries dependency responsibility. It does not prove every public API has a customer; API compatibility still needs API diff, compilation fixtures, or customer-scenario tests.
Package-content safety checks
Contracts extract nupkg files and reject product-core .cs, non-template .csproj, PDB, Source Link configuration, local absolute paths, private-key markers, and common credential markers. Template packages may contain Consumer shell source but not Framework, Platform, or Licensing core namespaces.
This is a delivery isolation line, not the only source-protection control. Package signing, feed authorization, minimal public API, legal terms, and release audit remain necessary.
Extending contracts for a new Profile
- register the packable project in the machine-readable commercial catalog;
- declare full package closure, host roles, generators, and License Features;
- create an external Consumer referencing the meta-package, never product projects;
- verify empty-cache restore, Release build, test, and publish;
- delete the Profile and key transitive packages and require restore failure;
- inspect template output and nupkg files for product implementation source;
- assert Generator/analyzer assets for every target host;
- add a separate trimmed-publish contract when trim/AOT support is claimed.
Contract-batch acceptance evidence
- logs identify commit, SDK, and configuration;
- temporary feed package set matches catalog projection;
- every Consumer uses
PackageReferenceonly; - four cache classes are isolated and source mapping is active;
- positive flow and deletion tests behave as expected;
- package scan finds no source, path, or secret leak;
- whether each Profile’s publish is trimmed is recorded per profile in the conclusion;
- formal release still hands immutable artifacts to the GA workflow.