项目:vitest-playground | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
本篇是三份"契约类"文档中的示例集:《03-接口设计》讲契约的设计哲学与字段语义(为什么三字段、错误四象限怎么划分),《15-接口全量参考》给全字段速查表与 16 章 56 课时索引(查字段、查课时用),本篇则提供可以直接复制重放的完整实例——每一个请求/响应对都对应真实课时与真实处理路径,每一份 Lesson 示例都取自 src/pages/vitest-learn/data.ts 的原文。读完 03/15 再读本篇,效果是"契约从定义变成手感"。示例的组织顺序遵循一条请求的生命:先看黄金路径(成功与断言失败),再看错误形态(429/超时/启动失败/400 家族),然后翻到数据侧看四种课时形态的完整 Lesson 实例与其装配产物对照,最后给一份"全字段拉满"的 RunResult 综合样例。
先立三条贯穿全篇的契约公理(出处见《05-核心流程》跨流程不变量,本篇不重复论证):其一,请求体只有 chapterKey / lessonKey / code 三个字符串字段,其余一切执行要素由服务端按课时数据推导;其二,"运行了但失败"一律 HTTP 200 + success:false,"根本没运行"才动用 4xx/5xx,且所有 JSON 错误体仍是 RunResult 形态(failResult 构造:{success:false, output, passed:0, failed:1});其三,success 的唯一判据是"没有失败用例",pending/skipped 不影响通关。
以 basics/first-test(1.1 第一个测试用例)为例,用户把初始代码的三个注释任务补全后点击运行。请求:
POST /api/run-vitest HTTP/1.1
Content-Type: application/json
{
"chapterKey": "basics",
"lessonKey": "first-test",
"code": "function add(a, b) {\n return a + b;\n}\n\ntest('add function should return correct sum', () => {\n expect(add(1, 2)).toBe(3);\n});\n\ntest('add(0, 0) equals 0', () => {\n expect(add(0, 0)).toBe(0);\n});\n\ntest('add(-1, 1) equals 0', () => {\n expect(add(-1, 1)).toBe(0);\n});\n"
}服务端处理序:字段校验通过 → findLesson('basics','first-test') 命中 → limiter.acquire() 获得槽位 → buildLessonFiles 产出 {'lesson.spec.ts': code}(无 grader,走普通分支)→ resolveRunOptions 得 {coverage:true, benchmark:false} → 沙箱写入 vitest.config.js(coverage enabled)+ setup.ts + lesson.spec.ts → spawn node node_modules/vitest/vitest.mjs run --root <runDir> --reporter=json --outputFile.json=result.json --coverage → 读 result.json 与 coverage/coverage-summary.json → finally 删除沙箱目录。响应(HTTP 200,字段按 parseVitestJson 的组装规则还原):
{
"success": true,
"output": "Test Results\n============\n\n📄 lesson.spec.ts\n ✓ add function should return correct sum (2ms)\n ✓ add(0, 0) equals 0 (1ms)\n ✓ add(-1, 1) equals 0 (1ms)\n\n通过: 3 失败: 0 跳过: 0",
"passed": 3,
"failed": 0,
"pending": 0,
"tests": [
{ "name": "add function should return correct sum", "status": "passed", "duration": 2, "file": "lesson.spec.ts" },
{ "name": "add(0, 0) equals 0", "status": "passed", "duration": 1, "file": "lesson.spec.ts" },
{ "name": "add(-1, 1) equals 0", "status": "passed", "duration": 1, "file": "lesson.spec.ts" }
],
"markers": [],
"coverage": {
"total": { "lines": 100, "statements": 100, "branches": 100, "functions": 100 },
"files": [
{ "file": "lesson.spec.ts", "lines": 100, "statements": 100, "branches": 100, "functions": 100 }
]
}
}三个字段细节值得指出:output 的图标与耗时格式(✓ 名称 (Nms))来自 parseVitestJson 第 171-181 行的映射规则,耗时四舍五入取整;tests[].duration 是未取整的毫秒原值,与 output 里的取整值可能相差 1ms,这不是 bug 而是两个消费者的精度需求不同;markers 成功时恒为空数组(不是 undefined)——因为 parseVitestJson 无条件返回该字段,前端 markers effect 拿到空数组正好走"显式复位"分支(见《18-代码走读实录》§3.3)。
同一课时,用户把第二个用例写错(expect(add(0, 0)).toBe(1))。响应骨架不变,关键差异在失败明细:
{
"success": false,
"output": "Test Results\n============\n\n📄 lesson.spec.ts\n ✓ add function should return correct sum (2ms)\n ✗ add(0, 0) equals 0 (1ms)\n AssertionError: expected 0 to be 1 // Object.is equality\n\n ✓ add(-1, 1) equals 0 (1ms)\n\n通过: 2 失败: 1 跳过: 0",
"passed": 2,
"failed": 1,
"pending": 0,
"tests": [
{ "name": "add function should return correct sum", "status": "passed", "duration": 2, "file": "lesson.spec.ts" },
{ "name": "add(0, 0) equals 0", "status": "failed", "duration": 1, "file": "lesson.spec.ts", "line": 9 },
{ "name": "add(-1, 1) equals 0", "status": "passed", "duration": 1, "file": "lesson.spec.ts" }
],
"markers": [
{ "line": 9, "message": "AssertionError: expected 0 to be 1 // Object.is equality" }
]
}生成规则对照:markers[].line 取 vitest 报告的 location.line(缺失时兜底为 1);markers[].message 取 failureMessages[0] 的首行且截断至 200 字符(parseVitestJson 第 196-199 行);失败用例在 output 里缩进附带 failureMessages[0] 的前 6 行(第 189-195 行)——所以 output 里的失败详情最多 6 行,而 marker 只有一行,两者信息量不同。普通课时(非 TDD)下前端把 markers 下发给 Monaco,形成第 9 行的红色波浪线;此课时带 coverage(普通课时默认开),此处省略其值(结构与 §1 相同)。
并发 2 满且排队 8 满时,limiter.acquire() 抛 RUNNER_BUSY,HTTP 层转 429:
HTTP/1.1 429 Too Many Requests
{ "success": false, "output": "运行排队已满,请稍后再试", "passed": 0, "failed": 1 }两个容易混淆的 429 来源要区分:本例是应用内信号量(createRunLimiter(2,8));另一个来源是 nginx 限流(每 IP 10r/m + burst 3,limit_req_status 429),其响应体是 nginx 的默认 HTML 而非 JSON——前端 runVitest 对非 2xx 统一合成 服务端返回 429 的失败结果,所以用户看到的文案相同,但排障时要分清是哪一层(《12-FAQ与故障排查》§3.3 的区分方法:应用内 429 只在排队真满时出现,nginx 429 在低频请求下也会触发)。
用户写了个死循环用例。60 秒后 killGroup() 以 SIGKILL 整杀进程组,超时分支构造失败结果:
HTTP/1.1 200 OK
{
"success": false,
"output": "Vitest 运行超时(60s,可能有用例陷入死循环):\nRUN v4.1.8 /Users/xx/vitest-playground/.vitest-runs/run-XXXX\n\n❯ lesson.spec.ts (0 test) [Run killed]\n...\n(此处为子进程输出尾部 2000 字符)",
"passed": 0,
"failed": 1
}注意三点:状态码是 200——超时属于"运行了但没通过";output 附带子进程输出尾部 2000 字符(out.slice(-2000))供排障;timeoutMs 参数即便被调用方注入也被 Math.min(timeoutMs, RUN_TIMEOUT) 钳制在 60s,HTTP 协议层不暴露该参数。该形态有专门集成用例(timeoutMs:2000 缩短验证)锁定"输出含超时 + 目录零残留"两个断言。
vitest 二进制不存在(未 pnpm install)时走 child.on('error') 分支:
{ "success": false, "output": "启动 Vitest 失败: spawn node ENOENT\n(请确认项目已安装 vitest:pnpm install)", "passed": 0, "failed": 1 }result.json 缺失且 stdout 截不出 JSON 时(罕见,多为环境异常):
{ "success": false, "output": "无法从输出解析测试结果,Vitest 输出:\n(尾部 2000 字符)", "passed": 0, "failed": 1 }result.json 存在但内容损坏时:测试结果解析失败,原始输出:\n + 前 2000 字符。这三条与超时共同构成《15-接口全量参考》§3 表的序 8-12"运行了但失败"家族。
四个变体共用 400 状态码,output 文案区分了具体环节:
HTTP/1.1 400 Bad Request
{ "success": false, "output": "请求无效: 请求体不是合法 JSON", "passed": 0, "failed": 1 }{ "success": false, "output": "请求无效: 需要 chapterKey / lessonKey / code 三个字符串字段", "passed": 0, "failed": 1 }{ "success": false, "output": "未知课时: basics/not-exist", "passed": 0, "failed": 1 }触发方式依次是:body 不是合法 JSON;body 为 {}、null、[] 或任一字段非字符串(含旧契约 {files: {...}} 形态——它没有三字段,被同一条件拦下,有专门测试锁定);课时 key 在 data.ts 中寻址不到。这条链的全部检查发生在触碰文件系统与并发槽位之前(快速失败链语义见《18-代码走读实录》§1.3)。
这是一个值得单独澄清的形态。文件名由服务端 buildLessonFiles 生成,正常情况下不存在"用户提交非法文件名"的入口;唯一可能触发 sanitizeEntryName 抛错的是课时数据被改出非法 extraFiles 名。按当前源码(vitestServerRunner.ts 第 302 行校验发生在 handleRunVitest 第 477 行 runInSandbox 调用内部,异常被第 479-481 行 catch 接住),其实际响应是:
HTTP/1.1 500 Internal Server Error
{ "success": false, "output": "服务端错误: 非法文件名: ../evil.ts", "passed": 0, "failed": 1 }即 500 而非 400——与《15》§3 表序 7("runInSandbox 装配/执行抛意外异常 → 500 服务端错误: {message}")一致;但《12-FAQ与故障排查》§3.6 曾将其表述为"三个 400 变体"之一,属文档间口径漂移,以源码行为为准(《12》§3.6 已于 2026-10-06 回刷修订为 500 口径,同源表述《01》§3.5、《04》§9、《07》§1.1、《08》§3-L3、《18》走读二亦已订正)。若未来把文件名校验提前到 handleRunVitest 的 try 之外,它才会变成 400——那是行为变更,需同步《15》表与测试。
coverage-advanced/benchmark 课时的响应走 parseBenchTable 路径(无 JSON reporter,解析 vitest bench 默认表格):
{
"success": true,
"output": "Benchmark Results\n================\n\n • 累加 1000: 0.002ms (345678 ops/s)\n • 累加 100000: 0.089ms (11234 ops/s)\n",
"passed": 2,
"failed": 0,
"tests": [
{ "name": "累加 1000", "status": "passed", "duration": 0.002 },
{ "name": "累加 100000", "status": "passed", "duration": 0.089 }
]
}三个特征:output 以 Benchmark Results 开头(与 test 模板区分);无 coverage 字段(bench 课时 resolveRunOptions 关闭 coverage);tests[].duration 是 bench 表格的 mean 值——注意其单位语义存在已立项的混乱(前端按毫秒渲染,实际为亚毫秒小数,《13-技术债务与演进建议》问题 12,⚠️ 待确认 vitest 4.1.8 bench mean 列的精确单位)。bench 用例抛错时默认 reporter 不打印表格行,解析得 0 行即返回失败(无法从输出解析基准结果),该行为有测试锁定。
以下五份示例全部取自 src/pages/vitest-learn/data.ts(行号随文标注),是"数据侧契约"的完整样本。理解它们的钥匙是 ADR-007:形态由字段组合推导,没有显式 mode 字段。每份示例的建议读法是三步:先看字段组合推出了什么形态(grader 有无、environment 值、benchmark 值),再看装配产物对照(哪些文件会出现在沙箱目录里、各自的内容来源是谁),最后回到 §1-§3 想象这个课时点击运行后响应会长什么样——三步走完,"数据结构 → 装配行为 → 响应形态"这条因果链就在同一个课时上闭合了。这也是调试课程数据时最可复用的推理顺序:课时表现不符合预期时,先确认形态推导是否如你所想,再确认装配产物是否符合形态,最后才怀疑执行与解析环节。
{
key: 'first-test',
title: '1.1 第一个测试用例',
description: '编写你的第一个 Vitest 测试',
code: `// Vitest 测试入门
// test() 或 it() 用于定义测试用例
// 待测试的加法函数
function add(a, b) {
return a + b;
}
// 编写你的第一个测试
test('add function should return correct sum', () => {
expect(add(1, 2)).toBe(3);
});
// 尝试添加更多测试用例!
// 1. 测试 add(0, 0) 应该等于 0
// 2. 测试 add(-1, 1) 应该等于 0
// 3. 测试 add(100, 200) 应该等于 300
`,
solution: `// 待测试的加法函数
function add(a, b) {
return a + b;
}
// 完整测试用例
test('add function should return correct sum', () => {
expect(add(1, 2)).toBe(3);
expect(add(0, 0)).toBe(0);
expect(add(-1, 1)).toBe(0);
expect(add(100, 200)).toBe(300);
});`,
tips: [
'test() 是 Vitest 的核心函数,用于定义一个测试用例',
'expect() 接收实际值,返回匹配器对象',
'toBe() 是严格相等匹配器,使用 Object.is 比较',
'it() 是 test() 的别名,语义更自然'
],
quiz: [
{
question: 'Vitest 中用于定义测试用例的两个等价函数是?',
options: ['test() 与 it()', 'describe() 与 expect()', 'assert() 与 should()', 'run() 与 check()'],
answer: 0,
},
{
question: 'toBe(3) 使用的是哪种相等判断?',
options: ['== 宽松相等', 'Object.is 严格相等', 'JSON 字符串比较', '深比较'],
answer: 1,
},
],
}形态推导:无 grader → 普通模式;无 benchmark → coverage 开启。装配产物只有一个沙箱文件 lesson.spec.ts = code(用户编辑的就是测试文件),加上运行器写入的 vitest.config.js 与 setup.ts。它同时展示了普通课时的"标配三件套":code(starter,允许教学性失败)、solution(check:lessons 强制实测通过)、tips/quiz(教学脚手架,与运行机制无关)。
{
key: 'tdd-fizzbuzz',
title: '14.1 TDD:实现 FizzBuzz',
description: '经典 TDD 练习。先阅读下方给定的测试用例,然后在编辑器中实现 fizzBuzz,让它全部通过。',
code: `// TDD 练习:实现 fizzBuzz
// 规则:
// - 能同时被 3 和 5 整除 -> 'FizzBuzz'
// - 能被 3 整除 -> 'Fizz'
// - 能被 5 整除 -> 'Buzz'
// - 否则返回数字本身(字符串形式)
export function fizzBuzz(n: number): string {
// TODO: 实现你的逻辑
return String(n);
}
`,
solution: `export function fizzBuzz(n: number): string {
if (n % 15 === 0) return 'FizzBuzz';
if (n % 3 === 0) return 'Fizz';
if (n % 5 === 0) return 'Buzz';
return String(n);
}
`,
grader: `import { fizzBuzz } from './lesson';
describe('fizzBuzz', () => {
test('非 3/5 倍数返回数字本身', () => {
expect(fizzBuzz(1)).toBe('1');
expect(fizzBuzz(2)).toBe('2');
expect(fizzBuzz(4)).toBe('4');
});
test('3 的倍数返回 Fizz', () => {
expect(fizzBuzz(3)).toBe('Fizz');
expect(fizzBuzz(6)).toBe('Fizz');
});
test('5 的倍数返回 Buzz', () => {
expect(fizzBuzz(5)).toBe('Buzz');
expect(fizzBuzz(10)).toBe('Buzz');
});
test('15 的倍数返回 FizzBuzz', () => {
expect(fizzBuzz(15)).toBe('FizzBuzz');
expect(fizzBuzz(30)).toBe('FizzBuzz');
});
});
`,
hiddenGrader: `import { fizzBuzz } from './lesson';
describe('fizzBuzz 隐藏校验', () => {
test('覆盖更多边界', () => {
expect(fizzBuzz(7)).toBe('7');
expect(fizzBuzz(9)).toBe('Fizz');
expect(fizzBuzz(20)).toBe('Buzz');
expect(fizzBuzz(45)).toBe('FizzBuzz');
expect(fizzBuzz(98)).toBe('98');
});
test('实现必须具有真实逻辑(不能用查表蒙混)', () => {
for (let i = 1; i <= 100; i++) {
const r = fizzBuzz(i);
if (i % 15 === 0) expect(r).toBe('FizzBuzz');
else if (i % 3 === 0) expect(r).toBe('Fizz');
else if (i % 5 === 0) expect(r).toBe('Buzz');
else expect(r).toBe(String(i));
}
});
});
`,
tips: [ /* 同 data.ts 原文,3 条,末条:'隐藏校验会检查 1~100 的全部情况,硬编码几组答案是行不通的' */ ],
quiz: [ /* 同 data.ts 原文,2 题 */ ],
}形态推导:有 grader → TDD。装配产物三个文件:lesson.ts = code(用户实现,注意 grader 里 import { fizzBuzz } from './lesson' 正是相对导入到这个文件)、lesson.spec.ts = grader(可见测试,前端只读展示)、lesson.hidden.spec.ts = hiddenGrader(隐藏校验,从不进入任何 HTTP 响应)。hiddenGrader 第二个用例是防作弊范本:1~100 全量遍历,硬编码查表必被拦下——判题器"防偷懒不防看答案"的边界(《07-模块详解》§4)由此例可见。TDD 课时的 markers 在前端被丢弃(行号属于 spec 文件),失败定位靠结果面板逐用例列表。
{
key: 'rt-greeting',
title: '15.1 渲染组件',
description: '实现 Greeting 组件:接收 name 属性,渲染 <h1>Hello, {name}!</h1>。用 render + getByText 验证。',
environment: 'jsdom',
code: `import { type FC } from 'react';
export const Greeting: FC<{ name: string }> = ({ name }) => {
// TODO: 返回 <h1>Hello, {name}!</h1>
return null;
};
`,
solution: `import { type FC } from 'react';
export const Greeting: FC<{ name: string }> = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
`,
grader: `import { render, screen } from '@testing-library/react';
import { describe, test, expect } from 'vitest';
import { Greeting } from './lesson';
describe('Greeting', () => {
test('渲染问候语', () => {
render(<Greeting name="Vitest" />);
expect(screen.getByText('Hello, Vitest!')).toBeTruthy();
});
});
`,
hiddenGrader: `import { render, screen } from '@testing-library/react';
import { describe, test, expect } from 'vitest';
import { Greeting } from './lesson';
describe('Greeting 隐藏校验', () => {
test('不同名字', () => {
render(<Greeting name="World" />);
expect(screen.getByText('Hello, World!')).toBeTruthy();
});
test('空名字', () => {
render(<Greeting name="" />);
expect(screen.getByText('Hello, !')).toBeTruthy();
});
});
`,
tips: [ /* 同 data.ts 原文,3 条 */ ],
quiz: [ /* 同 data.ts 原文,1 题 */ ],
}形态推导:environment:'jsdom' 叠加 grader → jsdom + TDD( 第十五章 3 个课时全部如此)。装配产物与 4.2 的骨架相同,但有两处环境差异:扩展名全部从 .ts 变为 .tsx(lesson.tsx / lesson.spec.tsx / lesson.hidden.spec.tsx),且 spec 与 hidden 文件头部注入 pragma // @vitest-environment happy-dom\n\n。配置侧的配套是 esbuild: { jsx: 'automatic' }(writeRunConfig 顶层键),让 JSX 无需 import React 即可编译;setup.ts 注入的 @testing-library/jest-dom/vitest 为 DOM 断言提供匹配器。注意 data.ts 第 19 行 environment 字段注释写的是"注入 // @vitest-environment jsdom",与实现注入的 happy-dom 有措辞出入(真实行为以 vitestServerRunner.ts 第 75 行为准)——字段名沿用历史称谓、执行环境用 happy-dom 实现,这层错位在《05-核心流程》流程三有专述。
{
key: 'benchmark',
title: '9.7 性能基准测试',
description: '用 bench() 测量函数耗时。开启基准模式后,vitest 以基准方式运行并报告均值与吞吐量。',
benchmark: true,
code: `import { bench, describe } from 'vitest';
function sum(n: number) {
let s = 0;
for (let i = 0; i < n; i++) s += i;
return s;
}
describe('sum 性能', () => {
bench('累加 1000', () => {
sum(1000);
});
bench('累加 100000', () => {
sum(100000);
});
});
`,
solution: `// 参考答案:bench 更适合做「实现方案对比」——同一语义,两种写法
import { bench, describe } from 'vitest';
// 循环累加:O(n)
function sumLoop(n: number) {
let s = 0;
for (let i = 1; i <= n; i++) s += i;
return s;
}
// 高斯公式:O(1)
function sumFormula(n: number) {
return (n * (n + 1)) / 2;
}
describe('求和性能对比', () => {
bench('循环累加 100000', () => {
sumLoop(100000);
});
bench('公式 100000', () => {
sumFormula(100000);
});
});
`,
tips: [
'bench 用于度量性能,不同于 test 的正确性断言',
'报告中的 mean 是平均耗时(ms),hz 是每秒操作数',
'基准结果会受机器负载影响,只作相对比较'
],
}形态推导:benchmark: true → bench 模式(全仓唯一一例)。装配仍是单个 lesson.spec.ts(bench 写在普通模式的 spec 文件里),但执行命令变为 node vitest.mjs bench --root <runDir>,沙箱配置追加 benchmark.include,coverage 关闭,响应走 Benchmark Results 模板(§3.6)。它是"同一份装配规则、不同执行分叉"的范本:buildLessonFiles 对 bench 课时与普通课时产出的文件表完全一致,分叉发生在 resolveRunOptions 与 spawnVitest 的参数层。上例 solution 处为省篇幅做了压缩标注,原文以 data.ts 第 2213-2237 行为准。
{
key: 'vi-mock',
title: '3.2 vi.mock() 模块模拟',
description: '模拟整个模块的导入',
code: `// vi.mock() 用于模拟整个模块
// 注意:需要将模拟代码写在模块顶部
// ...
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' })
}));
test('mocked fetchUser', async () => {
const { fetchUser } = await import('./api');
const user = await fetchUser(1);
expect(user.name).toBe('Test');
});
`,
// vi.mock 需要模块真实可解析,故额外提供一个 api 模块文件供 mock 替换
extraFiles: {
'api.ts': `// 真实 API 模块(测试中会被 vi.mock 替换)
export const fetchUser = (id: number) => Promise.resolve({ id, name: \`User \${id}\` });
export const getUser = (id: number) => Promise.resolve({ id, name: \`User \${id}\` });
export const createUser = (data: { name: string }) => Promise.resolve({ id: 99, ...data });
export const deleteUser = (id: number) => Promise.resolve(true);
`,
},
/* solution/tips 同 data.ts 原文 */
}形态推导:无 grader → 普通;extraFiles 让装配产物多出 api.ts(Object.assign 并入文件表)。它的存在理由写在源码注释里:vi.mock('./api', ...) 需要模块真实可解析,mock 才有替换对象。extraFiles 的文件名同样要过 sanitizeEntryName 白名单(课时数据写非法名会 500 拒绝,见 §3.5),前端则以只读 Tabs 展示这些文件(chapter.tsx 第 381-401 行)。
把所有可选字段一次性拉满的综合样例(实际响应不会同时含 coverage 与 bench 特征,此例用于字段速查;逐字段语义表见《15》§4):
{
"success": false,
"output": "Test Results\n============\n\n📄 lesson.spec.ts\n ✓ 用例一 (1ms)\n ✗ 用例二 (2ms)\n AssertionError: expected 1 to be 2\n\n○ 已跳过用例\n\n通过: 1 失败: 1 跳过: 1",
"passed": 1,
"failed": 1,
"pending": 1,
"error": "(预留字段,当前无生产者与消费者)",
"tests": [
{ "name": "用例一", "status": "passed", "duration": 1, "file": "lesson.spec.ts" },
{ "name": "用例二", "status": "failed", "duration": 2, "file": "lesson.spec.ts", "line": 5 },
{ "name": "已跳过用例", "status": "skipped", "file": "lesson.spec.ts" }
],
"markers": [
{ "line": 5, "message": "AssertionError: expected 1 to be 2 // Object.is equality" }
],
"coverage": {
"total": { "lines": 83.87, "statements": 84.21, "branches": 66.67, "functions": 75 },
"files": [
{ "file": "lesson.spec.ts", "lines": 100, "statements": 100, "branches": 100, "functions": 100 },
{ "file": "api.ts", "lines": 75, "statements": 76.9, "branches": 50, "functions": 66.67 }
]
}
}字段速读:pending 来自 numPendingTests,skipped 用例计入其中且不影响 success 判据;tests[] 与 markers[] 由同一次 assertionResults 遍历派生(一个全量、一个只收失败);coverage.files[] 的 file 取 coverage-summary.json 各键的 basename,四项 pct 缺省补 0;error 是唯一"从未被生产"的字段——出现即可视为协议演进占位。前端消费映射(谁渲染什么)见《03-接口设计》§7 的生产/消费对照表。
所有请求示例都可以用 curl 重放,这是核对文档与源码是否漂移的最快手段。黄金路径的重放命令(本地 pnpm dev 或 pnpm preview 均可,端口 8000):
curl -s -X POST http://localhost:8000/api/run-vitest \
-H 'Content-Type: application/json' \
-d '{"chapterKey":"basics","lessonKey":"first-test","code":"test(\"ok\", () => { expect(1 + 1).toBe(2); });"}' | python3 -m json.tool把 body 换成 §3 各例的形态即可重放错误路径(429 需并发占满两个槽位再发 11 个请求,建议用 for i in $(seq 1 11); do curl ... & done 观察;nginx 429 的重放需在生产域名上 6 连发,见 docs/deploy/runbook.md 的验证命令)。响应示例的核对基线是 tests/runner-security.test.ts 的全链路用例与《15》§3 的错误码表——当三者(文档示例、测试断言、实际响应)出现不一致时,以测试断言为准并修订文档,这正是本套文档"与测试同源"的维护约定(《13》结尾的文档债务条款)。