Files
jkooe 6206645f31 fix(ci): D2 覆盖率口径改为 include 白名单(数字不再被运行环境稀释)
## 缺陷

`server` 的 `test:coverage` 只写了阈值,没有 include/exclude。某些开发机
(IDE / 代理 / 沙箱工具)会在 Node 的 `require` 链上注入 `*.cjs` 钩子,这些
**项目外**文件被覆盖率跟踪器一并统计:

```
# broker-ipc-client.cjs      | 24.76
# node-brokered-fs-shim.cjs  | 50.13
# node-language-shim.cjs     | 82.86
# node-safe-delete-shim.cjs  | 41.91
# all files                  | 76.56   ← 被稀释 10+ 个点
```

危害是双向的:
① 换机器/换环境数字就变,**不能作为交付证明**;
② 60% 门禁在拿被稀释的分母判定 —— `server.js` 真掉 20 个点可能被这些 shim 抵消
   而不报警。

## 修法(用 include 白名单,而不是逐个 exclude 环境特有文件名)

```json
"test:coverage": "node --test --experimental-test-coverage \
  --test-coverage-include='**/server/*.js' \
  --test-coverage-exclude='**/node_modules/**' \
  --test-coverage-exclude='test/**' \
  --test-coverage-lines=60 test/*.test.mjs"
```

`include` 表达的是正确语义:「只统计本项目源码」。逐个 exclude 环境特有的
`*.cjs` 名字换台机器就会再冒出来,不解决问题。

## 三条验收(逐条实测)

1. **报告内无项目外文件**:覆盖率报告的文件列只有 10 个项目源码
   (config / crypto / db / errors / logger / migrate / ratelimit / server / tokens /
   validate .js),`grep -E "Applications|/Users/|node_modules|\.cjs"` 对报告段落
   零命中。
2. **数字可复现**:连跑 3 次,`all files` 均为 `89.29 | 70.85 | 88.65`。
3. **门禁真实会 fail**:阈值临时调到 99 复跑 → `exit=1` 并打印
   `Error: 89.29% line coverage does not meet threshold of 99%.`
   (改回 60 后恢复正常)

## 连带

- `.github/workflows/ci.yml`:把口径写进覆盖率那一步的注释(为什么用 include、
  为什么不能靠 exclude 兜底),本地与 CI 从此同一口径。
- `README.md`:覆盖率与测试数全部换成实测值 ——
  server **89.29%**(原 75.31% 是被污染的数字)、launcher **91.67%**(原 91.06%);
  server 测试 **52**(原 43)、launcher 测试 **451**(原 390);
  并新增「覆盖率口径为何用 include 而不是 exclude」小节,把这次事故写清楚。
2026-10-26 01:59:46 +08:00

119 lines
4.3 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Only in One - CI(R-P1-23)
#
# 门禁顺序刻意「先快后慢、先静态后动态」:
# 1. 类型检查 + Lint + Prettier 格式校验(秒级失败,最快给出反馈)
# 2. launcher 单测 + 覆盖率门禁(含核心四模块 ≥70%)
# 3. server 单测 + 覆盖率门禁(≥60%)
# 4. 冒烟测试(真实起服务跑全链路)
# 5. 残留进程检查(防止冒烟测试漏下常驻进程)
#
# 注意:本工作流**不引入**任何密钥;server 的 JWT_SECRET 由 smoke.mjs 内部随机生成。
name: ci
on:
push:
branches: ['**']
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: typecheck + tests + coverage + smoke
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Print toolchain versions
run: |
node -v
npm -v
# ---------------- 依赖(两个子项目各自独立)----------------
- name: Install server deps
working-directory: server
run: npm ci
- name: Install launcher deps
working-directory: launcher
run: npm ci --ignore-scripts
# ---------------- 1) 类型检查 + Lint + 格式 ----------------
# `tsconfig.typecheck.json` 同时覆盖 src 与 test(主 tsconfig.json 只含 src,
# 属 build 用途),因此测试文件的类型漂移也会在这里被拦住。
- name: Typecheck launcher (src + test)
working-directory: launcher
run: npm run typecheck
# lint 首次实测 9.07s、二次 7.28s(< 10s 阈值,故与 typecheck 同列,不拆 lint:ci)。
# 见 docs/packaging.md「代码规范与静态检查」。
- name: Lint launcher
working-directory: launcher
run: npm run lint
- name: Prettier format check
working-directory: launcher
run: npm run format:check
# ---------------- 2) launcher 单测 + 覆盖率门禁 ----------------
# thresholds 写在 launcher/vitest.config.ts:
# · 全局 lines ≥60%
# · connect / discover / usb / agent / share / platform/* ≥70%
# 低于阈值时 vitest 以非 0 退出,CI 直接失败。
- name: Launcher tests + coverage gate
working-directory: launcher
run: npm run test:coverage
# ---------------- 3) server 单测 + 覆盖率门禁 ----------------
- name: Server tests
working-directory: server
run: npm test
# 口径写在 server/package.json 的 test:coverage 里:
# --test-coverage-include='**/server/*.js' ← 只统计本项目源码
# --test-coverage-exclude='**/node_modules/**'
# --test-coverage-exclude='test/**'
# 为什么不靠 exclude 兜底:本地开发机上运行时的 require 钩子(如 IDE / 代理
# 工具注入的 *.cjs shim)会被覆盖率跟踪器一起统计,把分母稀释掉 10+ 个点,
# 于是「server.js 掉 20 个点」可能被 shim 抵消而不报警。用 include 白名单后
# 数字与机器无关、可复现(本地与 CI 同一口径)。
- name: Server coverage gate (lines >= 60%)
working-directory: server
run: npm run test:coverage
# ---------------- 4) 冒烟测试(真实起服务 + 全链路)----------------
- name: Smoke test
working-directory: server
run: npm run smoke
# ---------------- 5) 残留进程检查 ----------------
# smoke.mjs 会在结束时 close server + exit;若有残留说明清理逻辑坏了。
- name: Assert no leftover processes
run: |
if pgrep -f 'smoke\.mjs' >/dev/null 2>&1; then
echo '::error::smoke.mjs 退出后仍有残留进程'
pgrep -af 'smoke\.mjs' || true
exit 1
fi
echo 'no leftover process'
# 覆盖率报告作为构建产物留存,便于事后排查(不参与判定)
- name: Upload launcher coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: launcher-coverage
path: launcher/coverage/
if-no-files-found: ignore
retention-days: 7