关于 dsh-ci-test-reliability
用于设计能适应 CI 并发和平台差异的 DSH 测试技能,重点涵盖资源隔离、生命周期清理、状态同步及回归验证。
主要功能
- 01梳理重叠的 CI 执行拓扑
- 02原子分配端口和路径
- 03隔离并恢复全局状态
- 04依据可观测状态进行同步
- 05等待资源彻底释放
- 06验证回归并避免掩盖不稳定问题
适用场景
- 01避免并行测试进程间资源冲突
- 02可靠测试子进程和服务器清理
- 03诊断平台相关的 CI 测试故障
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ deepseek-ai/ deepseek-harness --skill dsh-ci-test-reliability安装后在 Claude Code 中输入/dsh-ci-test-reliability确认已加载。
附带文件references/ci-flake-diagnosis.md
详细介绍
可靠的 DSH CI 测试
构建的测试应在仓库实际的 CI 拓扑下保持正确,而不只是单独在安静的工作站上运行时正确。本技能负责隔离和可靠性决策;它不会取代仓库的测试分层策略,也不会为推送选择所有命令。
阅读适用规则
- 使用测试策略选择单元测试、覆盖率、预期输出、快照、浏览器或真实 API 证据。
- 使用防御性模式处理生命周期、子进程、取消和清理行为。
- 当工作进程或作业拓扑会影响测试时,阅读当前生效的 Vitest 配置和 GitHub 工作流。
- 对于录制会话场景,还应遵循快照说明。
- 测试设计合理后,使用dsh-pre-push-checks选择待推送的验证项。
建模执行拓扑
除非当前生效的配置能够证明不会重叠,否则应假设以下各层可能同时运行:
- 同一个 Vitest 文件中的测试。
- 不同的 Vitest 文件或工作进程。
- 同一个作业中的独立 Vitest 进程或仓库门禁进程。
- Runner 共用一台主机的不同 Actions 作业。
进程隔离无法隔离主机端口、可预测的文件系统路径、外部服务、数据库、套接字或继承的子进程。对于每项已获取的资源,都要明确其所有者、原子分配机制、可观察的就绪信号、已注册的清理操作,以及表示操作已静止完成的信号。
不要仅仅因为某个测试夹具缺乏隔离,就串行执行整个测试套件。应先缩小独占范围或更改资源分配方式。Vitest 顺序执行代码块无法保护主机资源不受其他文件、进程、作业或 Runner 的影响。
原子分配资源
使用资源所有者提供的分配器,不要先检查资源是否可用,再稍后尝试占用。
- 网络测试夹具使用
listen(0)绑定回环地址,并且仅在服务器报告已监听后读取分配的地址。绝不要扫描空闲端口后再绑定。 - 使用
mkdtemp为每个测试创建私有临时根目录;不要占用可预测的共享路径。 - 为共享数据库、套接字、会话和输出位置分配每个测试独有的命名空间。
- 路径必须不存在时,使用独占创建。
- 将稳定的录制标识符与临时传输地址分开。在测试夹具内部进行转换,而不是强制实时资源使用录制值。
仅用作解析器输入或预期值的字面路径和 URL 不属于已获取的资源。不要仅仅因为它们看起来是固定值就改写它们。
限制进程级全局状态
将 process.env、cwd、模拟计时器、区域设置和时区、模块模拟、注册表、控制台钩子、globalThis 以及对全局 fetch 的拦截视为独占的可变资源。
优先使用注入的依赖或实例级适配器。必须修改状态时:
- 记录原始值是不存在还是存在;
- 精确恢复到原始状态;
- 立即注册恢复操作;
- 在最小的修改范围内使用
try/finally; - 如果局部
finally之前可能发生失败,则保留afterEach兜底; - 只拦截测试夹具所拥有的最精确请求或调用。
尊重平台自身的语义
CI 会在 Windows 和 POSIX 主机上运行同一套测试,而操作系统拥有的值不一定会以测试写入时的样子返回。
- 只有当断言能够容忍回写失败时,回写才是安全的。将文件的
mtime恢复原值,以证明指纹无论如何都会失效,这种做法在所有平台上都成立;但将它恢复原值以证明记录仍然有效,则假设了无损往返,而 NTFS 的 100 纳秒刻度无法精确保留小数毫秒。当断言依赖恢复结果时,应从新读取的值中获取预期值,而不是使用先前记住的值。 - Windows 对环境变量名不区分大小写,因此在测试固件中将
http_proxy和HTTP_PROXY作为两个独立键写入,在 Windows 上只会保留一项。 - Windows 会异步释放文件句柄,因此在 POSIX 主机上能立即完成的重命名或删除操作,需要设置有上限的重试,时长应根据观察到的竞争情况确定。
- Windows 不具备 POSIX 的权限或信号语义。依赖这些语义的测试用例应明确跳过相应平台并说明原因,而不是在所有平台上放宽断言。
优先采用在所有平台上都成立的观察方式。如果确实无法做到,应明确排除相应平台。
根据 CI 队列的预算设置超时
describe 或测试用例的超时会覆盖运行器的 --testTimeout,而不是服从它。因此,低于 CI 队列预算的值会缩短 CI 已分配的时限;同一个固定值在默认值更短的主机上,却会扩大时限。受进程创建约束的套件应采用 CI 队列预算;若使用更短的时限,必须说明原因。
提升测试预算时,也要提升钩子预算。设置和清理会承受同样的竞争压力,因此只提高测试用例预算,只会把竞争导致的失败转移到 afterEach。
当超时本身是测试对象时,外层等待时间应远大于被测超时。证明 20 ms 的截止时间会触发的测试用例,不应与测试框架自身的等待时间发生竞争,否则负载会决定哪个截止时间先报告。
根据状态进行同步
固定时长的睡眠不能证明设置已完成或清理已稳定。
- 等待明确的就绪事件、握手、状态转换、由测试持有的 Promise,或可从外部观察到的条件。
- 使用延迟 Promise 或屏障将竞态安排在确定的时点,并证明相关操作确实重叠。
- 仅使用超时为等待设定上限,绝不能把超时作为断言成立的条件。
- 除非调度顺序就是待测的产品行为,否则不要断言依赖调度器的执行顺序。
- 当测试对象是时间本身时,应注入或模拟时钟,并始终恢复真实计时器。
清理至静止状态
获取资源后应立即注册清理,以确保断言失败时也能释放资源。清理应停止新的回调或请求、移除监听器、恢复全局钩子、终止所拥有的任务,并等待子进程退出、服务关闭、工作线程终止或等效的完成信号。
调用 abort()、close() 或 kill(),却不等待所拥有任务的完成信号,属于清理不完整。如果可能发生延迟完成,应证明释放资源后不会再修改其他测试。
证明预期的回归
- 条件允许时,先观察修复前普通回归测试失败。
- 对于新的静态检查或语料库检查,暂时引入被拒绝的情况,并观察预期的失败。
- 对于竞态,使用屏障证明操作发生重叠;仅重复执行不算竞态测试。
- 对于端口、套接字、共享路径、子进程或其他主机资源,如果修复涉及跨进程隔离,则应并发运行相互独立的测试进程。
- 如果测试夹具启动的子进程有自己的截止时间,应先断言子进程未因信号或超时而结束,再断言其退出状态,以便被终止的子进程报告为超时,而非状态不匹配。
- 验证外部状态、事件、文件、日志、退出状态或释放情况,而不是相信组件的自我报告。
压力测试可补充确定性回归测试,但不能替代它。
拒绝掩盖不稳定测试的修复
对于确定性的本地测试,不要将以下做法当作根因修复:
- 未明确等待的状态就延长超时时间;
- 添加重试;
- 将所有文件改为串行执行;
- 吞掉错误或未处理的拒绝;
- 放宽断言;
- 通过规范化消除不稳定行为;
- 在清理或断言前添加休眠。
根据真实 API 策略,对于文档中说明的外部提供方瞬时故障测试,重试仍然有效。将这一例外限制在外部边界。
恢复预算不算掩盖问题。将测试套件的预算提高到其原本已有的通道预算,或将有界重试的规模设置为运行器上实际测得的争用程度,都明确指出了等待的工作,并恢复通道所提供的预算;两者都不会在未检查等待原因的情况下凭空增加余量。
诊断现有的不稳定测试
对于现有的概率性 CI 故障,请阅读 CI 不稳定测试诊断工作流。仅要求诊断时应保持只读:报告原因和证据,除非用户也要求修复。
验证并报告
针对受影响的行为运行范围最小的回归测试。仅当更改涉及以下风险时,才补充对应拓扑的证据:
- 全局状态变更需要提供恢复证据;
- 生命周期或子进程相关工作需要提供静默拆除的证据;
- 端口、套接字或共享路径需要提供并发独立进程的证据;
- 新增的检查需要提供负向对照。
推送前,请使用 dsh-pre-push-checks。准确报告命令和观察到的结果;不要将重试、跳过的测试或待处理的 CI 描述为通过。