ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Resume-Matcher 测试策略与验证方案:从「绿勾即剧院」到 444 个确定性测试与本地 pre-push 门禁

Resume-Matcher 测试策略与验证方案:从「绿勾即剧院」到 444 个确定性测试与本地 pre-push 门禁 Resume-Matcher 测试策略与验证方案从「绿勾即剧院」到 444 个确定性测试与本地 pre-push 门禁【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher本文基于仓库内 docs/agent/testing-strategy.mdStatus: Living document2026-05-30 起稿于test/backend-coverage-foundation分支base 为dev展开。该文档记录了 Resume-Matcher 后端apps/backendPhase 1–6与前端apps/frontendvitest§8的一次系统化测试审计与整改计划其门禁是本地pre-push钩子而非 PR CI。文章将以原文档为骨架结合 apps/backend/pyproject.toml、.githooks/pre-push、apps/backend/tests/ 下各测试模块源码完整还原这一「从评估到落地」的测试治理实践并给出可直接复制的运行命令、覆盖缺口地图与阶段路线图。Resume-Matcher 是一个本地运行、支持 100 LLM 的简历/PDF/求职信 AI 工具箱。本文的核心主题是当「构建在合并后才被发现坏了」「用户反馈 Ollama 不工作、简历渲染不出来」而现有测试套件却从未被任何自动化执行时如何用证据驱动的方式重构测试策略。读完本文你将掌握如何用覆盖矩阵定位「真实覆盖 vs 缺失覆盖」、如何区分确定性测试与 LLM Eval 并让两者各司其职、如何用respx在 HTTP 传输层复现「Ollama 不工作」类回归、以及如何用.githooks/pre-push本地钩子代替 PR CI 守住dev/main的绿。1. TL;DR这篇文档在说什么原文档用五句话点明了全部结论先整体复述仓库已经存在一套真实的后端测试套件192 个测试基于pytestpytest-asynciohttpx。不需要换框架。运行时191 通过、1 失败、54% 行覆盖率。唯一失败项test_health_returns_degraded是一个过时测试而非产品缺陷——而它一直红着的事实恰恰证明这套套件从未被自动化运行过。「构建坏了没人发现」的根因没有 PR 门禁 CI。唯一的 workflow 是docker-publish.yml合并到main时构建并推送镜像合并前没有任何东西运行pytest、tsc、next build或 lint。现有测试集中在确定性算法核心diff 引擎、schema而把整个 I/O 面DB、LLM、解析器、Playwright全部 mock 掉了。因此覆盖在有的地方是真实的但在用户真正受伤的地方LLM 调用、PDF 渲染、上传解析恰恰缺失。需要两类测试且它们是两回事确定性测试管道正确性、LLM 被 mock、每次改动都跑和EvalsLLM 输出质量、真实/录制 LLM、按需/夜间跑。当前只有第一类且仅覆盖核心第二类为零。2. 现状评估先量化再下结论原文档的评估命令不需要修改pyproject.toml——用临时插件即可得到覆盖数据cd apps/backend uv run --with pytest-cov pytest -q --covapp --cov-reportterm-missing # 192 tests · 191 passed · 1 FAILED · 54% coverage · ~6s结合 apps/backend/pyproject.toml 可以看到该套件的既有正确配置asyncio_mode auto无需手动pytest.mark.asyncio、--strict-markers未注册的 marker 直接报错、unit/service/integration/eval四个显式声明的 marker以及addopts [--strict-markers, -v, -m, not eval]默认排除 LLM-as-judge eval。2.1 那一个失败项是「金丝雀」apps/backend/tests/integration/test_health_api.py::test_health_returns_degraded期望GET /health在 LLM 不健康时返回degraded。但 apps/backend/app/routers/health.py 重构后/health变成了纯 liveness 探针永不调用 LLM直接return HealthResponse(statushealthy)就绪性readiness改由GET /status承担。测试从未同步更新。更讽刺的是它的兄弟用例test_health_returns_healthy是错误地通过了——它 mock 了check_llm_health而该端点现在根本不再调用它mock 是死的、断言是空心的。结论这里一个绿勾毫无意义一个红叉无人看见——这正是生产事故背后的失败模式。在 apps/backend/app/routers/health.py 的源码注释里可以印证这一设计决策「Lightweight liveness check for Docker HEALTHCHECK. Does NOT call the LLM provider. Use GET /status for full LLM health.」2.2 覆盖地图哪些真实、哪些缺失这是原文档给出的模块级覆盖基线表逐行继承并标注含义模块覆盖解读services/improver.pydiff 引擎84%✅ 真正扎实——路径解析、apply/verify diffs、skill 门控schemas/models.py88%✅ 真实services/refiner.py72%✅ 尚可routers/config.py53% 仅契约级llm.py47% 纯函数辅助已被测真实请求 check_llm_health Ollama 路径未测routers/enrichment.py38% 仅测了 regenerate 匹配其余被 mockconfig_cache.py38%database.py34% 此基线下真实 DB 未被执行——集成测试 mock 了dbPhase 4 7 已改services/cover_letter.py26% 完全未测services/parser.py20% 上传→markdown→JSON 未测连纯日期恢复都未覆盖pdf.py渲染20% 「简历渲染不出来」这一类的源头routers/resumes.py18% 最大文件1,796 LOCtailor PDF CRUD 几乎全裸2.3 关于集成测试的结构性真相192 测试基线时点在 192 测试基线时每一个apps/backend/tests/integration/*测试都 patch 了app.routers.x.db以及 LLM/解析调用。它们验证的是状态码、请求校验、响应形状和路由分支逻辑如 API-key 掩码、regenerate 回退匹配——是有价值的契约测试但它们没有证明数据库真的持久化、Playwright 真的渲染、markitdown 真的解析、任何 provider包括 Ollama真的响应。(Phase 4 与 7 之后新增了真实 SQLite 的isolated_db持久化及 pipeline/tracker 集成测试。)3. 框架决策保留 pytest增量补齐原文档的决策明确保留pytestpytest-asynciohttpx它已正确配置了asyncio_modeauto、严格 markers、unit/service/integrationmarkers按需增量添加需求工具为什么覆盖作为可追踪数字pytest-cov量化差距与增量停止猜测对假 provider含 Ollama测真实llm.pyrespx或pytest-httpx在 HTTP 传输层 mock让真实的 routing/normalization 代码跑起来——回归「Ollama 不工作」的唯一方式PDF 渲染证明Playwright已是依赖一条 smoke test → 从 print 路由得到真实 PDF 字节Prompt/skill 质量仓库内eval harnessGolden fixtures 结构化 scorer 可选 LLM-as-judge推送门禁本地替代 PR CIpre-pushgit hook.githooks/跑后端套件 locale parity红则阻止推送无 PR 触发 CI——见 §5 Phase 6在 apps/backend/pyproject.toml 中可以看到这些工具已作为可选依赖落定dev [pytest8.0.0, pytest-asyncio0.24.0, httpx0.28.0, respx0.21.1]另有独立可选组e2e-monitor [pypdf4.0.0, httpx0.28.0]。3.1 确定性测试 vs Evals关键区分确定性测试Deterministic test——LLM 被 mock对已知响应断言代码行为。快每次改动都跑。回答的是「管道正确吗」Eval——真实或录制LLM 调用按 rubric 打分。非确定性、花钱花时间夜间/按需运行绝不进 PR 门禁。回答的是「这次 prompt 改动让输出变好了吗」「我的 prompt 改动是否有帮助」无法用确定性测试回答——你需要 evals反过来也绝不应拿非确定性 eval 去卡 PR。两层分开各司其职。3.2 Prompt 链skills及每一阶段如何被测试原文档给出了完整的技能流水线upload → parse_resume_to_json → extract_job_keywords → generate_skill_target_plan → verify_skill_target_plan (verify 纯、确定性门) → generate_resume_diffs [strategy: keywords | nudge | full] → apply_diffs (纯) → render_resume_pdf对每一阶段确定性层——与措辞无关、必须成立的结构性不变量合法 JSON/schema、不捏造雇主/日期真实性规则、每个 section 都被保留、JD 关键词确实出现在输出中、diff 路径可解析。绝大多数「prompt 改动搞坏了什么」的回归在这里被免费拦住。Eval 层——golden(resume, job_description)fixtures → 用真实模型跑该阶段 → 用 rubric 打分启发式 LLM-as-judge。跨时间与跨 prompt 编辑追踪质量。这一分层在 apps/backend/tests/evals/scorers.py 有完整实现五个纯函数 scorer详见下文 §6零 LLM、零网络、零磁盘构成第一道廉价防线。4. 什么是「验证我们最近的工作」三种具体机制对本次倡议中的每一次变更原文档要求三件事覆盖增量Coverage delta。每个批次报告其触及模块的前后覆盖数字。要数字不要感觉。反剧院检查Anti-theater check。对关键逻辑的新测试确认当代码被破坏时该测试确实失败一次快速手动变异。这是test_health_returns_healthy「错误地通过」陷阱的解毒剂。钉住近期 PR 的回归测试。第一批新覆盖刻意锁定最近交付的行为让「我们最近的工作」拥有安全网_normalize_api_base——/v1/v1重复路径去重与 OpenAI 保持原样issue #751。resolve_api_key—— 安全规则ollama/openai_compatible不得回退到环境变量LLM_API_KEY防止付费 key 泄露给本地服务器。get_model_name——ollama_chat/前缀与 OpenRouter 嵌套前缀。上传时空提取文本的拒绝resumes.py:546PR #794即 apps/backend/app/routers/resumes.py。restore_dates_from_markdown—— 月份在 LLM 解析后幸存。这些纯函数回归测试落在 apps/backend/tests/unit/test_llm_providers.py例如get_model_name(_cfg(ollama, llama3)) ollama_chat/llama3、OpenRouter 的openrouter/anthropic/claude-3.5-sonnet嵌套前缀、以及「不双重加前缀」的守卫——每一条都直接对应上表的一次生产事故或安全问题。5. 分阶段路线图Phase 1–7原文档以图例✅ done · in progress · ⬜ planned完整记录了每一阶段的产出与验证数字逐项继承如下Phase 1 — 基础 廉价确定性覆盖PR #820 →dev✅ COMPLETE✅ 审计 本文档✅ 让套件变绿修复过时的health测试liveness vs readiness✅llm.pyprovider/Ollama 纯函数回归测试apps/backend/tests/unit/test_llm_providers.py✅parser.py纯测试日期恢复apps/backend/tests/unit/test_parser.py 空文本拒绝apps/backend/tests/integration/test_upload_api.py✅ 真实 SQLitedatabase.pyCRUD 测试apps/backend/tests/unit/test_database.py✅ 验证192 → 265 测试、1 个静默失败 → 0、覆盖 54% → 58%database 34→96%、parser 20→72%、llm 47→55%、health 重新有意义。反剧院变异检查通过。Phase 2 — 传输契约测试LLM/Ollama✅ COMPLETEapps/backend/tests/integration/test_llm_contract.py8 个测试✅respx支撑对假 Ollama OpenAI-compatible HTTP 服务器走真实complete/complete_json/check_llm_healthbase-URL 处理 #751、线上 JSON 提取、thinking-tag 剥离、健康检查错误码映射 密钥清洗。发现litellm 1.86 默认用 respx 看不见的 aiohttp 传输 → 测试设置disable_aiohttp_transportOllama 发出两次调用/api/show探针 /api/chat。llm.py55% → 74%。test_llm_contract.py的实现细节值得一提每个测试都是真实的 respx HTTP 测试——litellm 的客户端真的序列化请求、经 httpx 传输发出、解析 mock 响应全程不 mockrouter.acompletion/litellm.acompletion边界。其 autouse fixture_litellm_httpx_transport解释了一个真实世界的坑litellm 1.86 默认的LiteLLMAiohttpTransport会让请求绕过 respx 直飞真实网络必须monkeypatch.setattr(litellm, disable_aiohttp_transport, True)强制落回 httpx并 flushin_memory_llm_clients_cache。TestOllamaTransport还验证了关键行为能力探针永远打localhost:11434/api/show而真正的补全必须打到用户配置的{api_base}/api/chat——测试断言http://ollama.test:11434/api/chat被命中这正是「Ollama 配置了自定义地址却总连 localhost」类 bug 的回归网。TestCheckHealthTransport则覆盖了健康检查的密钥清洗fake provider 在 401 错误体中回显sk-key测试断言error_detail中原始 key 与部分前缀都不可见、必须出现redacted。Phase 3 — 渲染安全网 ✅ COMPLETEapps/backend/tests/integration/test_pdf_render.py11 个测试✅ 真实 headless-Chromium 渲染自包含的data:URL → 断言真实%PDF字节纯辅助测试format/marginsconnection-refused →PDFRenderError映射。无 Chromium 时渲染测试干净地 skip。pdf.py20% → 54%。Phase 4 — 端到端 pipeline ✅ COMPLETEapps/backend/tests/integration/test_pipeline_e2e.py5 个测试✅ 真实 routers 真实临时 DBisolated_db每个 LLM 边界被 mockupload → jobs → fetch以及preview→confirm 定制握手。断言真实持久化状态master 不变量、parent_id关联、improvements记录。resumes.py18% → 53%。这里的isolated_dbfixture 在 apps/backend/tests/conftest.py 有完整实现用Database(db_pathtmp_path / isolated_db.db)替换全局db单例并monkeypatch掉app.database及resumes/jobs/enrichment/config/health/applications/resume_wizard七个 router 模块中导入的db。fixture 注释强调了一个 SQLite 陷阱必须用临时文件而非:memory:因为连接池会给每个连接独立的 in-memory DBasync sync 两套 engine 将无法共享状态。同文件还提供了sample_resume完整ResumeData兼容字典、sample_job_keywords、sample_changes覆盖 replace/append/reorder 全部 action 类型的ResumeChange列表等共享夹具。Phase 5 — Eval harness结构化 LLM-as-judge✅ COMPLETEapps/backend/tests/evals/31 个 scorer 测试 1 个门控 judge✅ 纯结构化 scorersections_preserved、no_fabricated_employers、jd_keywords_present、is_valid_resume、personal_info_unchanged golden fixtures每个都在好与坏输入上被验证。✅ LLM-as-judge 标记为pytest.mark.eval使用开发者自己配置的 key被默认运行排除addopts -m not eval按需uv run pytest -m eval运行。无 key 时干净 skip。apps/backend/tests/evals/README.md 给出了 scorer 的完整语义表sections_preserved保证有内容的顶级 section 在定制后不消失no_fabricated_employers逐条比对workExperience中公司名、返回捏造的雇主列表空列表 真实jd_keywords_present返回 JD 关键词实际出现在定制简历中的比例0–1大小写不敏感子串匹配空关键词列表恒为 1.0is_valid_resume用ResumeData.model_validate校验personal_info_unchanged要求身份块逐字节不变。其测试test_scorers.py对每个 scorer 都构造了已知坏输入证明其真的会开火删掉一个 section →False、编造公司 → 被返回、改名字 →False——这就是反剧院的证据。golden fixtures 位于tests/evals/golden/cases.py的GOLDEN_CASES列表每个条目包含original/job_description/jd_keywords/tailored_good/tailored_bad五要素且要求tailored_good对每个关键词都达到 1.0、tailored_bad故意违反至少一条不变量追加新用例即被参数化测试自动拾取。Phase 6 — 本地 pre-push 门禁替代 PR CI✅ COMPLETE.githooks/pre-push✅ 版本化的pre-push钩子运行后端套件 免 Node 的 locale-parity 检查红则阻止推送。每克隆激活一次git config core.hooksPath .githooks绕过git push --no-verify。见 .githooks/README.md。✅刻意避免 GitHub Actions PR 门禁——仓库外部贡献者 PR 流量大PR 触发 CI 会对每一个含不可信代码都跑一遍。本地钩子以零成本为维护者自己的推送守住dev/main的绿。⬜可选未来基于 Node 的tsc/next build检查——因 nvm-in-hook 脆弱性而推迟纯 Python 的 locale-parity 守卫已覆盖已知的 i18n 破坏。.githooks/pre-push 的实际逻辑set -uo pipefail下依次运行三件事——①uv run pytest -q -p no:cacheprovider后端uv不存在则报错并置 status1②python3 scripts/check_locale_parity.py纯 Python验证apps/frontend/messages/*.json与en.json结构一致这正是曾让next build在合并后才炸掉的 i18n 不匹配③vitest run仅当node与本地 vitest 二进制都存在时执行否则警告跳过。全部检查总是运行以一次看到所有失败任一失败即exit 1中止推送。Phase 7 — SQLite 持久化 tracker 加密 key 覆盖PRs #841 #843 →main✅ COMPLETE✅ 持久层从 TinyDB 迁到SQLiteasync SQLAlchemyconftest.py::isolated_dbfixture 现跨所有 router 模块替换为可丢弃的临时文件 SQLiteDB不再是 TinyDBapps/backend/tests/unit/test_database.py演练真实 SQLite CRUD含TestApplications。✅ 新 tracker 覆盖apps/backend/tests/integration/test_applications_api.pyCRUD、列分组、删除简历后的 detail 容错、批量移动/删除与apps/backend/tests/integration/test_tracker_autocreate.py确认一次定制自动创建applied卡片。✅ 加密的按 provider API keyapps/backend/tests/unit/test_crypto.pyFernet 加密/解密往返 掩码。✅/status优雅降级#843apps/backend/tests/integration/test_health_api.py扩展——每项检查互相隔离单个探针失败返回 200 partial/degraded 状态而非 500。这在 apps/backend/app/routers/health.py 有直接源码对应get_status将 LLM 健康探针与数据库统计各自包在独立的 try/except 中任一个失败只降级自己的字段_EMPTY_DB_STATS兜底保证/status仍能响应degraded而非 500最终status字段由llm_healthy and has_master_resume计算为ready或setup_required。✅ 验证默认uv run pytest数量现为~444此前 ~320。respx仍为llm.pymock HTTP 传输层。Phases 1–7 之后的结果192 → ~444 确定性测试 1 个 opt-in LLM-judge eval0 失败。Phases 1–5 由并行子代理构建每阶段一个、严格文件所有权使用dispatching-parallel-agents技能Phase 7 跟随 TinyDB→SQLite 迁移PRs #841 #843。6. 如何运行原文档给出的完整命令集在apps/backend下执行cd apps/backend # 完整确定性套件LLM-judge evals 通过 addopts -m not eval 自动排除 uv run pytest # 覆盖率临时插件不改 pyproject uv run --with pytest-cov pytest -q --covapp --cov-reportterm-missing # 按需的 prompt 质量 evals——结构化 scorer 总是运行 # LLM-judge 仅在配置了 LLM key 时运行使用开发者自己的 key否则跳过 uv run pytest -m eval # 单个模块 uv run pytest tests/unit/test_parser.py -q注uv run pytest不受项目 nvm/npm 约束影响——它纯 Python。前端tsc/build/lint 分开运行不在本后端阶段范围内。uv run pytest -m eval的行为细节来自apps/backend/tests/evals/README.md无 key 的干净运行会显示 scorer 测试通过 judge 测试skipped_needs_key()是测试第一行keyless 环境永远不会发出未门控的真实调用要真正运行 judge按运行应用的方式配置 provider/keyenv 或 Settings UI →data/config.json后重跑即可。7. 决策日志Decisions log日期决策理由2026-05-30以dev而非main作为本倡议基线。dev←main同步后从dev切分支工作合回dev。用户指示——在到达main之前在dev上批量完成这项工作。2026-05-30暂不引入 CI workflow仅测试。用户指示。CI 是 ROI 最高的修复但它是独立的显式决策且.github/workflows/受变更控制。2026-05-30Eval 层 结构化 LLM-as-judgejudge 使用开发者提供的 LLM key缺 key 时跳过。用户指示——开发者通常是维护者提供 key因此配置后接受真实 LLM 打分。2026-05-30保留pytest新增respx、pytest-cov、Playwright smoke、eval harness。现有框架是对的补缺口而不是换框架。2026-05-30用本地pre-push钩子做门禁而非 PR 上的 GitHub Actions。维护者外部 PR 流量大PR 触发 CI 会对所有 PR含不可信代码都跑。本地钩子以零成本让维护者自己的推送守住dev/main绿——后端套件 免 Node 的 locale parity——使用.githooks/core.hooksPath。8. 前端测试套件apps/frontend使用vitest Testing Libraryjsdom——运行npm run test或./node_modules/.bin/vitest run。后端同样的严谨度被应用先评估现状一个 65 测试的绿套件覆盖download-utils与两个组件再覆盖最高价值的未测逻辑。新增apps/frontend/tests/i18n-utils.test.ts——t()引擎getNestedValue点路径 缺 key 回退、applyParams替换。i18n-locale-parity.test.ts——构建破坏的套件内守卫每个messages/*.json必须与en.json结构一致镜像scripts/check_locale_parity.py。已验证反剧院向en.json加一个 key 会让全部四个 locale 失败。keyword-matcher.test.ts—— JD↔简历关键词提取/分段/匹配统计。section-helpers.test.ts—— section 排序、自定义 section ID、仅本地化未改动的默认值。html-sanitizer.test.ts—— DOMPurify XSS 白名单strong/em/u/a。api-client.test.ts——lib/api/clientURL 解析 超时/AbortErrorfetch被 stub。净效果65 → 117 前端测试全部绿。pre-push门禁在 Node 可用时运行该套件完整的tsc/next build门禁仍是未来工作nvm-in-hook 脆弱性。9. 未决问题 / 未来✅前端 locale-parity 测试—— 已完成i18n-locale-parity.test.ts 钩子里的scripts/check_locale_parity.py。一旦 I/O 面被广泛覆盖再决定每个模块的覆盖下限避免用单一全局 % 掩盖缺口。Node-aware 的tsc/next build门禁捕获 locale drift 之外的 TS 错误——推迟需要可靠的 node-in-hook。若重新考虑 GitHub Actions只对dev/main的 push 运行不对 PR。10. Agentic 端到端监控按需、仅报告在确定性套件与本地 pre-push 门禁之上还有一个agentic E2E monitor——一个 opt-in、按需的 harness驱动真实运行的应用master resume → 3–4 份定制变体 → PDFs捕获持久化的证据包日志 每个中间 JSON PDFs并用 Claude Code skill 以三个运行时任务评判输出质量、flow/render 完整性、provider 真实性。它是报告而非门禁——只提供信息、从不阻止推送、永不接入 CI。设计docs/superpowers/specs/2026-06-01-agentic-e2e-monitor-design.md计划docs/superpowers/plans/2026-06-01-agentic-e2e-monitor.mdharness 与操作手册apps/backend/e2e_monitor/README.md。OSS 安全harness 依赖是可选的额外项uv sync --extra dev --extra e2e-monitor每一步都被RM_E2E_MONITOR1 已配置 key 门控可运行 skill 被 gitignore其源码是已提交的 apps/backend/e2e_monitor/AGENT_PLAYBOOK.md。开发者真实 SQLite DB 永不被触碰隔离的DATA_DIR。运行cd apps/backend RM_E2E_MONITOR1 uv run python -m e2e_monitor sweep然后bash e2e_monitor/install_skill.sh并调用monitor-e2eskill 生成报告。结语这套策略的通用价值Resume-Matcher 的测试治理实践可以被提炼为四条可移植的原则先量化后整改覆盖矩阵而非印象、确定性测试与 Eval 严格分层前者进门禁、后者按需跑、在真实传输层复现用户报障respx而非 mock 内部边界、用本地钩子替代昂贵的 PR CI把守维护者自己的推送而不拖累外部贡献者。从「192 个测试、1 个看不见的失败、54% 覆盖」到「~444 个确定性测试、0 失败、I/O 面逐步补全」这份 living document 本身就是一个可回放、可验证的整改样本——任何「测试存在却从未被执行」的仓库都可以按同样的节奏从审计开始走出测试剧院。【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters more, locally with 100 LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表