项目:vitest-playground | 版本:v1.0 | 生成日期:2026-10-06 | 依据:源码分析
| 项 | 要求 | 依据 |
|---|---|---|
| Node.js | >= 22.0.0 | package.json 的 engines 字段;运行器依赖 node:child_process 的 detached 进程组与 process.kill(-pid) |
| 包管理器 | pnpm 10.34.5(packageManager 字段锁定) | AGENTS.md 铁律 3:只用 pnpm,禁止生成 package-lock.json;误生成须删除 |
| 操作系统 | macOS / Linux / Windows 均可(运行器对 win32 有 SystemRoot/COMSPEC 环境变量兜底) | vitestServerRunner.ts 的 buildChildEnv |
| 磁盘/内存 | 普通即可;运行器输出累计上限 1MB、请求体上限 5MB、并发 2,资源占用可控 | vitestServerRunner.ts 常量 |
| 外网 | 运行测试不需要外网(本地 vitest 离线执行);仅安装依赖时需要 | README.md「运行注意」 |
# 0) 进入项目(路径以实际为准)
cd vitest-playground
# 1) 安装依赖(vitest 必须就位:运行器直接 spawn node_modules/vitest/vitest.mjs)
pnpm install
# 2) 开发模式启动(dev 服务器 :8000,已挂载 /api/run-vitest 与 COEP/COOP 头)
pnpm dev
# 打开 http://localhost:8000 ,进入任意课时点击"运行测试"验证链路
# 3)(可选)类型检查 / 自测 / 课程质检
pnpm tsc # 仅类型检查,无 emit
pnpm test # 本项目自身测试(vitest run)
pnpm check:lessons # 逐课时质检课程代码(需依赖已安装)
# 4) 生产构建 + 预览(preview 同样挂载 /api/run-vitest,可验证生产链路)
pnpm build # 产物输出 dist/
pnpm preview # vite preview --port 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); });"}'
# 预期返回 {"success":true,"passed":1,...}本项目的显式环境变量面刻意极小,这是运行器安全设计的一部分:
| 变量 | 侧 | 是否必设 | 说明 |
|---|---|---|---|
PATH | 子进程白名单 | 是(继承宿主) | buildChildEnv 从 process.env.PATH 透传,缺省 /usr/local/bin:/usr/bin:/bin |
LANG | 子进程白名单 | 否 | 透传,缺省 C.UTF-8 |
NODE_ENV | 子进程 | 固定 test | 运行器硬编码,不接受外部覆盖 |
HOME | 子进程 | 固定为沙箱目录 | 指向 runDir,测试内 os.homedir() 落在沙箱 |
SystemRoot / COMSPEC | 子进程(仅 win32) | 否 | Windows 下透传,保证子进程可正常工作 |
其余 process.env 变量 | — | 绝不继承 | 防止宿主密钥经测试代码外泄;tests/runner-security.test.ts 用哨兵变量验证无泄漏 |
CI / GITHUB_ACTIONS | 质检脚本子进程 | 脚本内剔除 | scripts/check-lessons.ts 显式解构丢弃,避免 CI 模式改变 vitest 行为(.only 拒绝、快照缺失判失败等假失败) |
⚠️ 待确认:docs/superpowers/plans/2026-09-28-container-isolation.md 规划了 VITEST_RUNNER_BACKEND=local|docker 双后端切换,但当前 vitestServerRunner.ts 源码中未实现该变量——计划状态,非现存配置。除上述外,应用本身不读取任何 secret 类环境变量;vite.config.ts 不读取秘密(容器化方案约束,同样适用于现状)。
manualChunks 三分:monaco(monaco-editor + @monaco-editor/react)、antd(antd + icons)、react-vendor(react/react-dom/react-router-dom),配 chunkSizeWarningLimit: 2500。AGENTS.md 铁律 4:不要引入其它重型依赖或改坏该拆分;resolve.alias:@ → src/,@root → 项目根(vitest.config.ts、tsconfig.json 保持了同样的别名映射);server.headers 与 preview.headers 均设置 Cross-Origin-Embedder-Policy: require-corp 与 Cross-Origin-Opener-Policy: same-origin。README.md 说明其用途是 Monaco Worker 同源加载与一致性(WebContainer 已移除但头保留);铁律 5 要求生产宿主配置同样的响应头;preview.allowedHosts: ['vitest-playground.zhangzhengyang.com']。核心约束(AGENTS.md 推论):测试能力依赖 Vite dev/preview 服务器及其自定义中间件,纯静态托管(GitHub Pages、Vercel 静态等)跑不了测试。必须用承载了 /api/run-vitest 中间件的 Node 服务托管(pnpm preview、自建 Node 容器等);若只做静态部署,须明确告知用户测试功能不可用。
生产实际形态(docs/deploy/runbook.md 2026-09-28 澄清):NAS Docker 容器 vitest-playground-web(4800→8000)→ frpc 隧道 → VPS nginx 反代(443 → 4800),域名 vitest-playground.zhangzhengyang.com。发布更新流程:git pull && pnpm install --frozen-lockfile && pnpm build,重启 Node 进程(建议 systemd/pm2 托管,避免裸 nohup;NAS 侧长构建须 setsid 脱离 ssh 会话,验证只认容器/镜像/指纹证据)。
nginx 限流(必须配置),节选自 docs/deploy/runbook.md:
limit_req_zone $binary_remote_addr zone=vitest_run:10m rate=10r/m;
location /api/run-vitest {
limit_req zone=vitest_run burst=3 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8000; # 容器形态下上游为 4800
proxy_read_timeout 600s; # 2 并发消化 8 深排队最坏约 300s,600s 留缓冲
}
location / { proxy_pass http://127.0.0.1:8000; } # COEP/COOP 由应用响应头携带,勿重复 add_header验证限流:6 次连续 curl -X POST .../api/run-vitest,约第 4~5 个起应出现 429。发布核对清单(AGENTS.md):install 含 vitest、pnpm tsc 无错、pnpm build 无 chunk 告警、pnpm preview 能跑测试、生产宿主有 COEP/COOP、nginx 已限流。
| 症状 | 排查 | 处置 |
|---|---|---|
| 跑测试一直 429 | pgrep -af 'vite' 确认 preview 进程;pgrep -af 'vitest.mjs' | wc -l 看是否有卡死的 vitest(正常应为 0) | 有卡死进程则确认日志后重启 preview |
| 磁盘增长 | du -sh .vitest-runs 应接近 0 | 大量残留说明强杀后 finally 未执行:rm -rf .vitest-runs/run-* 并排查 |
| 站点能开但报"无法连接运行服务" | preview 未启动或 nginx 上游端口不符 | 启动进程/修正 upstream |
| 历史密钥泄露面 | tr '\0' '\n' < /proc/<PID>/environ | grep -iE 'key|token|secret...' | 现行代码已不继承 env,但需一次性确认历史暴露期无密钥泄漏;有则立即轮换 |
本项目"以 Vitest 教 Vitest,也以 Vitest 测自己"。自测配置在 vitest.config.ts:environment: 'happy-dom'、globals: true、setupFiles: ['./tests/setupTests.ts']、include 限定 src/pages/vitest-learn/** 与 tests/**(排除 Ant Design Pro 模板残留)、coverage 用 v8 provider(reporter text/json/html,include src)、testTimeout: 15000、passWithNoTests: true。
| 文件 | 覆盖内容 |
|---|---|
tests/lessons.test.ts | 课程数据契约:章/课时 key 全局唯一、每章至少 1 课时、每课时 `code |
tests/progress.test.ts | computeBadges 五徽章边界(0 进度/1/10/过半/整章/全通/零课时特例)+ localStorage 进度读写/清除 |
tests/runner-security.test.ts | ★ 363 行的运行器防线全集(见 5.3) |
tests/setupTests.ts | 环境垫片:内存版 localStorage、URL.createObjectURL、MockWorker、MessageChannel polyfill、matchMedia、ResizeObserver(happy-dom + antd/Monaco 所需的最小浏览器面) |
该文件对 vitestServerRunner.ts 做"单元 + 真跑集成"双层验证,是修改运行器时的安全网:
sanitizeEntryName:放行合法课时名;拒绝路径穿越(../、反斜杠、绝对路径)、受保护配置覆盖(含大小写变体 VITEST.CONFIG.JS)、tsconfig.json、非字符串/空名;buildChildEnv:键集合恰为 HOME/LANG/NODE_ENV/PATH,哨兵变量 RUNNER_SENTINEL 不泄漏;runInSandbox 集成(真 spawn vitest):合法用例通过;env 无泄漏;用户 stdout 打印 JSON 不污染解析;死循环 2s 超时 kill 且 RUNS_ROOT 零残留;21 个文件超 MAX_FILES 拒绝;coverage 开启时返回覆盖率摘要(回归用例);海量 stdout(2 万行×200 字符)进程稳定;createRunLimiter:满载排队、队满抛 RUNNER_BUSY、所有权转移语义(release 直移交槽位,active 不超 max)、冗余 release 无负计数;handleRunVitest:缺字段/类型错误/null/数组 body → 400;旧 files 契约 → 400;未知课时 → 400;5MB+ 请求体 → 400;真实课时全链路(basics/first-test 初始 code 必须跑通、coverage-advanced/benchmark bench 链路必须产出结果、bench 抛错不误判成功);parseBenchTable:样例表格解析、ANSI 色码剥离后解析、无表格行时失败。scripts/check-lessons.ts(pnpm check:lessons,即 node --experimental-strip-types scripts/check-lessons.ts)是课程内容的专属 CI 门禁,逻辑:
data.ts bundle 成临时 mjs 后导入(Node strip-types 无法处理嵌套模板字符串;esbuild 二进制从 node_modules/.pnpm 定位,pnpm allowBuilds 放行);buildRuns 复刻产品运行器的装配(普通/TDD/jsdom pragma/extraFiles 全对齐),在 os.tmpdir() 下建沙箱——以 fs.realpathSync 规避 macOS /var/folders 符号链接导致的模块解析失败,并把仓库 node_modules 以 junction 链接进沙箱使裸导入可解析;剔除 CI/GITHUB_ACTIONS 变量;solution 的课时:solution 实测必须 PASS;starter code 实测允许"断言失败"(教学性失败起点,计 WARN),但崩溃/语法错误/无 JSON 判 FAIL;code/grader/hiddenGrader/extraFiles 全部过 esbuild 语法门禁(只 transform),损坏计 FAIL,全过计 SYN(注释里写明历史真实缺陷形态:模板字符串少闭合导致整段答案损坏);vitest bench + 表格行正则判定;汇总 passed/failed/warned/syntax-ok,仅 failed>0 时退出码 1;支持传课时 key 过滤。.github/workflows/ci.yml 两个 job(push master / PR 触发,Node 22,pnpm cache):quality = install --frozen-lockfile → pnpm tsc → pnpm test → pnpm build;lessons = install → pnpm check:lessons。
gui-test-screenshots/ 目录保存的是GUI 黑盒测试的截图证据(非产品代码):t1_home.png(首页)、t2_lesson_page.png(课时页)、t3_run_pass.png(运行通过)、t6_quiz_after_click.png(小测作答)、t7_reset_rerun_pass.png(重置后重跑)、t10_progress.png(进度页)、badge_earned_state.png/badge_locked_state.png(徽章两种状态)。命名(t1~t10 编号)表明它们按测试用例编号归档,用于人审或回归比对;.gitignore 中的 .playwright-mcp/ 条目说明此类截图由 Playwright 驱动的浏览器自动化产出(⚠️ 驱动脚本本身未随仓库提交,属外部测试过程的产物)。
几组高频场景的最佳搭配:改运行器(vitestServerRunner.ts)时,先跑 pnpm test -- runner-security 只执行安全套件(vitest 支持文件名过滤),快速迭代通过后再全量 pnpm test;改课程数据(data.ts)时,用 pnpm check:lessons <lessonKey> 只体检被改的课时(脚本支持位置参数过滤),省下全量 56 课时的实测时间;调 UI 时 pnpm test:watch 配合 tests/progress.test.ts 这类纯逻辑用例做 watch 反馈,pnpm test:ui(@vitest/ui)则适合观察用例结构与耗时分布。两个提醒:其一,runner-security 套件里有多条真 spawn vitest 的集成用例(每条 15~60 秒超时预算),机器负载高时偶发超时失败,重跑即可,不要急于改代码;其二,vitest.config.ts 的 include 已把测试收敛到 src/pages/vitest-learn 与 tests/ 两个位置,新增测试文件请放在这两处,否则不会被 pnpm test 拾取。
/api/run-vitest;node_modules/vitest/vitest.mjs 不存在,重跑 pnpm install;.github/workflows/ci.yml 在 push master 与所有 PR 上触发,两个 job 相互独立、可并行:
job 1 quality(工程质量门禁):actions/checkout@v4 检出 → pnpm/action-setup@v4(版本取自 packageManager 字段)→ setup-node@v4(Node 22 + pnpm 缓存)→ pnpm install --frozen-lockfile(锁文件强校验,任何手改 pnpm-lock.yaml 不一致都会失败)→ pnpm tsc(strict 全量类型检查,覆盖 tsconfig.json include 的 src/tests/两个配置文件)→ pnpm test(vitest run,含 363 行运行器安全集成测试——注意这些用例会真 spawn vitest 子进程,CI 时长主要耗在这里)→ pnpm build(顺带验证 manualChunks 无体积告警)。
job 2 lessons(课程内容门禁):安装依赖后仅执行 pnpm check:lessons,对全部 56 课时逐一实测。它独立成 job 的意图明显:课程内容改动(改 data.ts)不应被工程问题阻塞评审,反之亦然;两个绿灯合起来才是完整的"可发布"信号。
本地复现 CI 的完整等价命令序列:pnpm install --frozen-lockfile && pnpm tsc && pnpm test && pnpm build && pnpm check:lessons。提交前跑一遍即可避免绝大多数 CI 失败往返。
开发期最常遇到的问题及最短定位路径:
pnpm dev 后点运行报"无法连接运行服务":确认访问的端口是 8000(vite.config.ts 的 server.port);若经反向代理/容器访问,确认 /api/run-vitest 路径被转发而非仅静态资源;node_modules/vitest/vitest.mjs 缺失,执行 pnpm install;用 pnpm 以外的包管理器安装过依赖也可能导致 .pnpm 目录结构异常,删掉 node_modules 重装;monacoSetup.ts 的 addExtraLib(VITEST_GLOBALS_DTS) 未生效,检查是否有改动破坏了 CodeEditor.tsx 顶部的 import './monacoSetup' 副作用导入;pnpm test 全量回归;chapterKey/lessonKey(进度键是 ${chapterKey}:${lessonKey},大小写敏感);environment: 'jsdom' 拼写正确——装配端靠它注入 happy-dom pragma,拼写错误会静默按 node 环境运行;pnpm check:lessons 报"未找到 esbuild 包":node_modules/.pnpm 下没有 esbuild 目录,重装依赖;该脚本按 esbuild@ 前缀探测版本化目录。package.json 中 vitest/@vitest/coverage-v8/@vitest/ui 三者必须同版本联动(当前 4.1.8 精确锁定)。升级前重点回归:--reporter=json 的输出结构(parseVitestJson 依赖 testResults[].assertionResults[] 形态)、--outputFile.json 行为、bench 默认 reporter 表格格式(parseBenchTable 的行正则对格式敏感)、// @vitest-environment pragma 语义;package.json 用的是 ^ 语义化范围,pnpm install 会随锁文件固定;大版本升级需同步检查 chapter.tsx 对 antd 组件 API 的使用(如 Alert 的 title 属性、Space 的 orientation)与 createBrowserRouter 的路由 API;.vitest-runs/ 是纯临时目录,无状态迁移负担,回滚无数据兼容问题;唯一的外部状态是用户浏览器里的 localStorage 进度(key 带 -v1 版本后缀,若未来进度结构破坏性变更,正确做法是启用 -v2 新 key 并迁移,而非原地改语义)。结合 AGENTS.md 的发布清单与 runbook,以下是可以逐项打勾的执行版:
pnpm install 无错误,node_modules/vitest/vitest.mjs 存在(运行器的硬依赖,缺它即"启动 Vitest 失败");pnpm tsc 零输出(strict 全绿)、pnpm test 全部通过(含真跑子进程的集成用例,本机约需一两分钟);pnpm check:lessons 输出 failed:0(warned 为教学性失败起点属正常,syntax-ok 为无 solution 课时通过语法门禁);pnpm build 成功且无 chunk 体积告警(manualChunks 三分生效,dist/assets/ 下应能看到 monaco/antd/react-vendor 独立 chunk);pnpm preview 后用本篇 §2 的 curl 验证 /api/run-vitest 返回 success:true;浏览器再过一遍普通课时与一个 TDD 课时;curl -sI https://<域名>/ | grep -i cross-origin);nginx 限流按 runbook 的 6 连 curl 法验证出现 429;origin 指向自己的仓库(AGENTS.md 提示默认仍是模板占位地址);dist/、.vitest-runs/、package-lock.json 不入库(前两者已在 .gitignore);public/sitemap.xml、public/llms.txt、public/robots.txt 与 index.html 的 canonical/OG 链接一致,public/og-cover.png 保持 1200×630。九项全绿即可发布。任何一项失败,优先回到本篇 §8 的排障手册定位,不要带病上线——尤其是第 6 项:公网上没有应用内鉴权的执行接口,nginx 限流是唯一的入口节流阀,AGENTS.md 铁律 2 也明确要求不得削弱、且更强隔离的方向是容器化而非回退共享目录模式。