Consumer 合同测试回答一个朴素问题:如果客户看不到产品仓库,只拿到模板和私有 Feed,项目还能不能构建和发布?
为什么普通集成测试不够
产品仓库里的 Host 可以直接引用项目,也能看到本机 NuGet 缓存。这样的测试容易掩盖三类问题:
- 某个 packable 包漏了依赖;
- Generator 只在产品仓库中偶然可见;
- Consumer 模板偷偷引用或复制了产品源码。
Consumer Contract 把这些便利拿走,再验证一次。
本地合同流程
读取商业包目录 → pack 候选包到临时 Feed → 创建外部 Consumer 项目 → 写入带 <clear /> 和 Package Source Mapping 的 NuGet.Config → 隔离 NuGet、HTTP、plugin 与 CLI cache → restore / build / test / Release publish → 检查包资产与源码暴露运行:
dotnet test tests/BitzOrcas.Framework.ConsumerContract.Tests \ --configuration Release \ --blame-hang-timeout 12m必须通过的断言
- Consumer 没有产品仓库
ProjectReference; - 输出不包含 Framework、Platform、Licensing 核心源码;
- 所有
BitzOrcas.*包来自映射 Feed; - Profile 的包闭包完整且版本一致;
- Generator 以 analyzer 资产进入正确 Host;
- build、test、Release publish 通过;
- 客户业务模块只能引用公开 Contracts/SDK seam。
典型失败
| 症状 | 可能原因 | 优先检查 |
|---|---|---|
NU1101 找不到包 | 包未 pack、目录漏项或 Source Mapping 不匹配 | 临时 Feed 内容、catalog、NuGet.Config |
| 编译期扩展方法缺失 | Generator 未作为 analyzer 进入目标 Host | Profile 的 generator/hostRoles 投影 |
| 本机通过、CI 失败 | 命中了本机全局缓存 | 隔离变量和缓存目录 |
| publish 失败 | 运行时资产、RID 或包依赖声明不正确 | 首个 publish 错误与包依赖图 |
| 发现产品源码 | 模板复制了禁止目录 | 模板内容和源码暴露断言 |
与正式 GA 的差别
本地合同从当前源码重新 pack,适合 PR 和开发反馈。正式 GA 不允许这样做:它必须下载可信发布产生的不可变包,从认证 HTTPS Feed 还原,并验证签名、provenance、SBOM 和策略证据。
当前覆盖矩阵
| 维度 | 本地 Consumer Contract | 正式 Commercial GA |
|---|---|---|
| 包来源 | 当前源码 pack 到临时目录 Feed | release workflow 的不可变 nupkg |
| Consumer 位置 | 系统临时目录 | release artifact 中的仓库外 Solution |
| Feed | 本地隔离 Feed + source mapping | 认证 HTTPS Feed + 短期只读凭据 |
| 缓存 | NuGet/HTTP/plugin/CLI 隔离 | 同样使用空隔离缓存 |
| 签名 | 不作为本地合同结论 | dotnet nuget verify --all + 指纹集合 |
| provenance/SBOM | 不验证正式证据 | schema、hash、策略和豁免到期检查 |
| publish | mini-api:PublishTrimmed=false;default-business:追加 Mediator 裁剪发布 | Release publish;目标项目设置决定裁剪 |
隔离为何要覆盖四类缓存
只清 NUGET_PACKAGES 不够。测试同时隔离:
NUGET_PACKAGES:全局包缓存;NUGET_HTTP_CACHE_PATH:HTTP 元数据和包响应;NUGET_PLUGINS_CACHE_PATH:Credential Provider/plugin 缓存;DOTNET_CLI_HOME:CLI 首次运行、模板和其他用户级状态。
测试 NuGet.Config 使用 <clear />,并把 BitzOrcas.* 映射到隔离 Feed。这样产品包不能从 nuget.org 或未声明的本机 source 偶然恢复。
# 运行完整合同集合;fixture 只 pack 一次,再由用例复制只读基线 Feed。dotnet test tests/BitzOrcas.Framework.ConsumerContract.Tests \ --configuration Release \ --blame-hang-timeout 12m \ --logger 'console;verbosity=normal'
# 只诊断 Mini API 外部消费主路径。dotnet test tests/BitzOrcas.Framework.ConsumerContract.Tests \ --configuration Release \ --filter 'FullyQualifiedName~Profile_Mini_Api_Should_Restore_Build_And_Publish'删除测试证明什么
正向 Restore 通过仍可能遇到“Profile 元包为空壳、Consumer 实际没有依赖它”的假阳性。合同因此复制 Feed、删除 Profile 包或闭包中的传递依赖,然后断言空缓存 Restore 必须失败。
删除测试回答“这个包是否真正承载依赖责任”。它不证明每个公共 API 都被客户使用;公开 API 兼容仍需要 API diff、编译夹具或客户场景测试。
包内容安全检查
合同会解压 nupkg 并拒绝产品核心 .cs、非模板 .csproj、PDB、Source Link 配置、本机绝对路径、私钥和常见 Credential 标记。模板包允许 Consumer 壳源码,但不允许 Framework、Platform 或 Licensing 核心 namespace。
这是一条交付隔离线,不是源码保密的唯一机制。还需要包签名、Feed 权限、最小公开 API、法律合同和发布审计共同工作。
新增 Profile 时怎样扩展合同
- 在机器可读 commercial catalog 登记 packable 项目;
- 在 Profile closure 声明完整包、Host role、Generator 和 License Feature;
- 为 Profile 创建外部 Consumer,引用元包而不是产品项目;
- 验证空缓存 Restore、Release Build、Test 和 Publish;
- 删除 Profile 包及关键传递依赖,断言 Restore 失败;
- 检查模板输出和 nupkg 内容没有产品实现源码;
- 把目标 Host 的 Generator/analyzer 资产纳入断言;
- 若声明 trim/AOT 支持,新增单独的裁剪发布合同。
合同批次的验收证据
- 测试日志记录提交、SDK 与配置;
- 临时 Feed 包集合与 catalog 投影一致;
- 所有 Consumer 只用
PackageReference; - 四类缓存隔离且 NuGet source mapping 生效;
- 正向流程和删除测试都按预期;
- 包内容扫描没有源码、路径或 Secret 泄漏;
- 每个 Profile 的 publish 是否裁剪在交付结论中逐条记录;
- 正式发布仍转交 GA workflow 验证不可变 artifact。