Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Mutation Testing

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
inputs:
full:
description: Force a full mutation run and rebuild the baseline
type: boolean
default: false

permissions:
contents: read

concurrency:
group: mutation-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
mutation:
runs-on: ubuntu-latest
timeout-minutes: 90
env:
MUTATION_NODE_VERSION: '24'
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.MUTATION_NODE_VERSION }}
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run source tests
run: pnpm run test:lib

# Stryker tracks src/lib and test*.ts. Hash helpers, adapters and configs
# separately so changes outside the mutation scope invalidate the baseline.
- name: Compute baseline compatibility key
id: baseline-key
env:
INPUTS_HASH: >-
${{ hashFiles('pnpm-lock.yaml', 'package.json', 'pnpm-workspace.yaml',
'.npmrc', 'stryker.config.*', 'vitest.config.*', 'tsconfig*.json',
'.github/workflows/mutation.yml', 'packages/**/package.json',
'packages/**/tsconfig*.json', 'packages/**/src/**', 'src/test/**',
'src/**/*.d.ts', '!src/test/test*.ts') }}
run: echo "prefix=mutation-core-v1-${RUNNER_OS}-node${MUTATION_NODE_VERSION}-${INPUTS_HASH}" >> "$GITHUB_OUTPUT"

- name: Restore main mutation baseline
id: baseline
uses: actions/cache/restore@v4
with:
path: reports/stryker-incremental.json
key: ${{ steps.baseline-key.outputs.prefix }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ steps.baseline-key.outputs.prefix }}-

- name: Run mutation tests
env:
FORCE_FULL: ${{ inputs.full == true }}
run: |
if [ "$FORCE_FULL" = "true" ]; then
pnpm run mutation:full
else
# Missing or incompatible baseline naturally falls back to a full run.
pnpm run mutation
fi

- name: Save main mutation baseline
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: actions/cache/save@v4
with:
path: reports/stryker-incremental.json
key: ${{ steps.baseline.outputs.cache-primary-key }}

- name: Upload mutation reports
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: reports/
if-no-files-found: warn
retention-days: 14
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ packages/*/src/*.d.ts
coverage/
.nyc_output/

# 变异测试(Stryker)
.stryker-tmp/
reports/

# 日志
*.log
npm-debug.log*
Expand Down
36 changes: 36 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,42 @@ instead」)。`@koa/router` 是官方维护的继任包,API 与 koa-router *
- **`zod` 从 `dependencies` 改为 `peerDependencies`**:作为 schema 校验库,erest 不应硬钉死 zod 版本,应由宿主项目统一提供。此前 erest 把 `zod@^4.0.5` 列为 hard dependency,导致任何 link/依赖 erest 的项目出现两份 zod 副本(erest 自带的 + 宿主的),TS 因 zod 版本字面量标记不同而报类型不兼容。**迁移**:宿主项目须在自身 `dependencies` 显式声明 `zod`(版本 `^4.0.0`)。adapter 子包(`@erest/express` 等)不直接依赖 zod,无需改动。


## v3.2.3 — 变异测试驱动的缺陷修复(文档生成器 / TestAgent / 工具函数)

接入 StrykerJS 变异测试(`pnpm run mutation`)后,补测过程中发现并修复以下问题:

### 修复(非 breaking)

**文档生成器(生成物正确性):**

- **`generate_axios`:路径参数模板转义错误** —— 生成的 SDK 中 `` `/user/\$\{id\}` `` 因 `\$` 转义使插值退化为字面量文本,**所有路径参数方法的 URL 参数实际不生效**。修复后生成 `` `/user/${id}` ``,参数正常插值。
- **`generate_swagger`:多参数路径只转换第一个占位符** —— `/:a/:b` 类路径的 `replace` 缺 `/g` 标志,只有 `:a` 变成 `{a}`。修复后全部转换。
- **`generate_swagger`:POST body 字段级 required 信息丢失** —— 此前字段必填信息被 `delete` 丢弃(相关收集代码为死代码)。修复后聚合进 body 参数的 `schema.required`(JSON Schema 语义)。
- **`generate_markdown`(schema):zod 4 下数组元素恒显示 `unknown[]`、枚举取值恒为空** —— 直接读 `_def.type`/`_def.values`(zod 3 形态)导致。已改走 `zod-meta.ts` 双轨访问器(`_def.element` / `entries`),Zod 3 与 Zod 4 下均正确渲染 `string[]` 与枚举值列表。
- **`generate_markdown`(schema):falsy 默认值渲染为空** —— `z.number().default(0)` / `z.boolean().default(false)` 的默认值列此前显示为空,现在如实展示 `0` / `false`。
- **`generate_markdown`(apis):无 input 的示例渲染出字面量 `undefined` 文本** —— 已修复为不渲染该行。

**TestAgent(测试代理):**

- **多个 `Set-Cookie` 响应头相互覆盖** —— `headers.forEach` 逐条回调使后值覆盖前值,session cookie 丢失。修复后用 `getSetCookie()` 取完整数组再合并(逗号拼接,与 cookie jar 解析约定一致)。
- **`attach()` 传流时内容不传输** —— Readable 直接 `form.append` 会被 undici FormData 隐式 `String()` 成 `[object Object]`。修复后流在请求时读尽为 Blob 再传输(multipart 文件名取字段名)。
- **GET/DELETE + `attach()` 直接抛 fetch 底层错误** —— 现在提前断言并给出中文提示(GET 不能携带 body,建议改用 POST/PUT/PATCH 或 `input()` 传 query)。

**工具函数:**

- **`getCallerSourceLine` 在 ESM 下恒返回空** —— 栈帧正则只匹配 CJS 括号风格,ESM/现代 V8 无括号帧(`at /path/file.ts:1:1`,且为 `file://` URL)永不匹配,API 文档的 sourceFile 字段静默失效。修复后双风格兼容并统一去掉 `file://` 前缀。
- **`jsonStringify` 的 replacer 为死代码** —— 防循环引用逻辑早已被注释,两分支行为恒同,已简化为直接 `JSON.stringify`。
- **`ErrorManager.modify()` 换 code 后旧 code 永久占用** —— 修复后旧 code 释放(可被再次注册),且新 code 若与其他错误冲突会立即报错(此前静默成功、错误暴露点错位)。

### 约定收敛

- **`_def` 读取全部集中到 `plugin/zod-meta.ts`**(AGENTS.md 约定 6):`generate_axios` / `generate_swagger` / `generate_postman` 此前的直读 `_def.typeName` / `_def.shape` / `_def.innerType` 已迁移到统一双轨访问器,并新增 `resolveZodShape` / `getZodElement` / `getZodEnumValues` / `getZodOptions` / `getZodDefaultValue` / `getZodLazyGetter` 导出。

### 变异测试基础设施

- 新增 `stryker.config.json` 与 `pnpm run mutation` / `mutation:incremental` 脚本(vitest runner + perTest 覆盖分析)。
- 变异分数从 44.93% 提升至 89.7%+(新增约 270 个内容级断言用例,全部融入现有测试文件)。

## v3.2.2 — 文档展示返回结果 schema(Swagger / Postman)

### 新增(非 breaking)
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,36 @@ examples 作为 pnpm workspace 子包,通过 `erest: workspace:*` 引用本地
自定义错误注册、自定义 type/schema 注册、`define()` 声明式、`mock()`、`response()` schema、
分层参数、文档生成、测试集成。详见 `examples/README.md`。

## 开发

```bash
pnpm install
pnpm run test:lib # 直接跑源码测试(ISLIB=1,无需先 build)
pnpm run test:cov # 覆盖率报告
pnpm run mutation # 变异测试(Stryker + vitest),默认增量;首次全量约 20-40 分钟
pnpm run mutation:incremental # 保留的增量命令,与 mutation 相同
pnpm run mutation:full # 忽略历史结果,强制全量重建基线
```

变异测试基于 [StrykerJS](https://stryker-mutator.io/)(配置见 `stryker.config.json`,变异范围为
`src/lib/**`),HTML 报告输出到 `reports/mutation/mutation.html`。变异分数衡量「测试集能否杀死
被注入的缺陷」,用于发现覆盖漏网与断言不足;分数参考即可,不必追求 100%。

变异测试需要 Node.js 22+(Stryker 10),本地增量基线保存到
`reports/stryker-incremental.json`。增量运行仍会先执行原始测试收集覆盖信息,再比较源码与测试变化。
修改依赖、配置、测试辅助文件或运行环境后,本地可用 `pnpm run mutation:full` 重建基线。

GitHub Actions 的 `Mutation Testing` 工作流使用 Node.js 24 和 pnpm 9:面向 `main` 的 PR
只读取兼容的主分支基线;推送到 `main` 后增量运行,成功才保存新基线。缓存键包含依赖、配置、
测试辅助文件及未参与变异的 adapter 源码,并为每次运行保存独立版本。基线缺失或兼容性输入变化时
自动全量执行,因此首次合并前的 PR 可能较慢;合并后首次成功的主分支 CI 才会建立完整共享基线。
PR 不写共享基线,主分支运行不会被后续提交主动取消。

需要强制重建时,在 Actions 中手动运行该工作流并勾选 `full`;仅在 `main` 成功运行时更新共享基线。
成功或失败时的可用 HTML、JSON 报告均上传为 `mutation-reports-*` artifact,保留 14 天。
静态变异或未跟踪的环境变化可能需要手动全量重跑。当前未设置最低变异分数门禁,作业成功仍可能
存在存活变异;此工作流也不会自动配置分支保护或 required status check。

## API 文档

线上文档站(typedoc 产物)由 CI 自动构建部署:
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "erest",
"version": "3.2.2",
"version": "3.2.3",
"description": "Framework-agnostic REST API framework with Zod validation, auto docs & test scaffolding. Express/Koa/@leizm/web adapters as separate packages.",
"type": "module",
"main": "dist/lib/index.js",
Expand All @@ -21,6 +21,9 @@
"test": "npm run build && npm run build:packages && vitest run && pnpm --filter erest-example test",
"test:lib": "export ISLIB=1 && vitest run",
"test:cov": "export ISLIB=1 && vitest run --coverage",
"mutation": "export ISLIB=1 && stryker run",
"mutation:full": "export ISLIB=1 && stryker run --force",
"mutation:incremental": "export ISLIB=1 && stryker run --incremental",
"test:examples": "pnpm --filter erest-example test",
"bench": "export ISLIB=1 && vitest bench --config vitest.bench.config.ts",
"tag": "git tag v`node -p 'require(\"./package\").version'`",
Expand Down Expand Up @@ -71,6 +74,8 @@
},
"devDependencies": {
"@leizm/web": "^2.7.3",
"@stryker-mutator/core": "^10.0.0",
"@stryker-mutator/vitest-runner": "^10.0.0",
"@types/debug": "^4.1.12",
"@types/express": "^5.0.0",
"@types/koa": "^2.15.0",
Expand All @@ -93,11 +98,6 @@
"vitest": "^3.2.4",
"zod": "^4.4.3"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"lint-staged": {
"*.{ts,js}": [
"oxfmt --write",
Expand Down
Loading
Loading