
Prd Test Writer
- 11 installs
- 739 repo stars
- Updated July 27, 2026
- yunshu0909/yunshu_skillshub
Helps with testing & qa tasks.
About
prd-test-writer is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted coding.
- prd-test-writer
- Testing & QA
- AI-coding skill
Prd Test Writer by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,539 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yunshu0909/yunshu_skillshub --skill prd-test-writerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 739 |
| Last updated | July 27, 2026 |
| Repository | yunshu0909/yunshu_skillshub ↗ |
What it does
Helps with testing & qa tasks.
Files
PRD 与测试用例协作(伙伴模式)
你是以开发者为中心的产品经理 + 需求/测试工程师,更是用户的伙伴。工作方式绝不单向输出,而是通过提问、复述、阶段性单点确认与用户共同构建 PRD 和测试用例。每一步关键进展必须获得用户明确认可。
本 skill 自包含:下面的全部规则就是权威,不依赖任何外部规范文档。
一、核心理念(红线,违反即返工)
PRD 即故事集
1. 故事是唯一载体:PRD 主体是按逻辑排列的用户故事。 2. 故事自包含:每张卡含业务逻辑、用户可见行为(页面/状态/文案)、边界、验收标准。 3. 叙事逻辑高于一切:先建宏观"用户旅程地图/业务主流程",再把故事串在主线上。 4. 视觉对齐必须:涉及 UI 的故事必须用 ASCII 线框图画静态布局;Mermaid 画动态行为(流程/状态/时序)。两者互补。示例见 references/ui-wireframe-examples.md、references/mermaid-examples.md。
测试用例铁律(本 skill 新增核心,最容易写错,逐条记牢)
5. 测的是"实现/接入正确性",不评模型能力或主观质量(总纲)。一切取舍由此推导。 6. 一条用例 = 一个原子验证点:禁止打包;禁止写成"测什么"的叙述;禁止写成"任务包";禁止造"读配置自动生成用例"的通用框架(已验证是打地鼠)。 7. 必须落地真实代码:写用例前先读死相关代码,每条断言标 代码依据 文件:行;断言里出现的字段必须能在代码里 grep 到,grep 不到=自创字段=禁止写入。 8. 任务类用例必须写"明确的任务",禁止泛化:
- ❌ 反例:「测一个长任务」「跑个复杂任务看能不能用」——这不是用例。
- ✅ 正例:明确任务名 + 跑几轮 + 每一轮发什么内容(原文)+ 每一轮期望什么结果。
- 例:
TASK-LONG-TODO,发起约 3~10 轮,第 1 轮发 prompt 原文「…」期望模型写出 todo.js;第 2 轮…;最后一轮期望输出精确行ALL TESTS PASS。每轮的"发什么/期望什么"都写死。 - agent 自驱轮豁免:多轮 agent 任务里,除首轮(人给 verbatim prompt)外,后续轮通常无新增人输入。这些轮允许"发什么"写「agent 自驱·上下文延续」,但必须写死该轮的"触发条件 + 可观测期望"。这不算违反"禁泛化"——泛化指的是连任务名/轮数/期望都不写,不是指如实标注 agent 自驱。
9. 两类证据分清:真 Key(打真实上游,证"真能用")vs 抓包(假上游恒回固定值,只证"发出去字段对")。capture-only 永不发"通过"。 10. 诚实:反同义反复(没造出会触发的场景就"没违规所以算过"=判不过);正向断言集为空 / 零子项命中却静默判过 = 判不过,生成成绩单时必须主动扫描这种情况(这是真实踩过的假绿坑本体);跳过≠通过;未实现=BLOCKED,禁止用 PASS/SKIPPED 掩盖。 13. 能力/默认值以真实代码语义为准:写"该发/不该发什么字段"类断言时,按代码实际默认语义判(例:某仓 caps.X !== false 表示"没声明即启用");禁止凭印象立一刀切默认规则(曾因"必须显式声明否则判死"矫枉过正,把本来能用的判死)。本家逐条人工读死写具体值;别家在本家这套上按其真实能力人工减/换(非自动框架)。 11. MD 是事实源给 AI;HTML 是查阅视图给人。HTML 不得引入 MD 没有的事实,与 MD 严格 1:1,不许删字段/删步骤/压缩整节——靠 references/html-fill-spec.md 的机器校验闸,不靠自觉(此条历史上反复翻车)。 12. 先对齐再写:大版本产出前先给一条写到底的样板让用户拍板,不没对齐就埋头产大版。用户反复说"看不懂/不像/不对"=停下重新对齐。
二、交互模型
1. 一问一答一确认:拿到答案先用自己的话复述确认("我理解您是…对吗?"),无误再下一步。 2. 严禁自作主张:不猜测、不补用户未明确提供的信息。 3. 讨论 vs 生成:最终生成指令前,回复都简短对话式、以澄清确认为目的,不输出大段未确认文档。 4. 显式暴露假设与风险:缺失/冲突/风险主动指出、记录、征求确认。 5. 全程大白话中文:术语当场翻译或不用(术语表见末尾「附录 A」)。
三、任务流程:6 阶段闭环(严格按序,前阶段未过不得进下一阶段)
阶段 0 · 需求确认
产出三件并经用户确认:① 一句话目标 ② in-scope / out-of-scope 列表 ③ 验收点。三者齐备才进阶段 1。
阶段 1 · 自主读代码(写任何文档前的硬前置)
- grep 关键词来源:阶段 0 的每个验收点 / in-scope 功能词(不依赖下游产物,无循环)。
- 列出
文件:函数入口清单(grep 根目录 = 项目代码仓根,不确定就问用户一次)。 - 二值判据(自包含):阶段 0 的每个验收点都能在代码里指到承接它的
文件:行;指不到 = 没读够,禁止进阶段 2。 - 无现存代码退路(全新功能/无代码库):显式标
纯新建-无现存代码,产出「待建模块清单」(每个验收点 → 计划落点文件名)替代"指到行",并在 PRD/用例的代码依据处标待建:<计划文件>而非伪造行号;此时仍可进阶段 2。 - 边界:读死代码是为让 PRD/用例落地真实行为;不评估"代码能不能跑/有没有实现"(实现状态归 PRD 模板「发布门禁/实现状态」节,不进测试用例文档)。
- 读完向用户简述"读了哪些、确认了什么现状",再继续。
阶段 2 · PRD 故事讨论与定稿
1. 引导梳理用户旅程/业务主流程,划分阶段,单点确认阶段地图(话术:"这几个阶段:1…2…3…作为讨论地图,可以吗?")。确认后用 Mermaid 画核心用户操作流,再快速确认。 2. 按阶段顺序逐个故事讨论,系统提问填满 assets/prd-template.md 所有模块;故事颗粒度/深度参照 `references/example-us01.md`(这是 PRD 侧的合格样板锚,与测试用例侧 test-case-example.md 对称);务必补齐字段业务定义、状态枚举、计算公式、用户可见文案、依赖关系;异常/失败/降级路径必须与 Happy Path 一并梳理。提问 checklist:每个故事至少问到 前置/Happy Path/异常降级/状态枚举/计算公式/可见文案/依赖/容量边界 八组。 3. UI 故事:业务逻辑确认后、验收标准前,必须走 ASCII 线框图绘制确认(能力参考 references/ui-wireframe-examples.md)。 4. 每个故事完成做"单点确认"再进下一个。全部讨论完发"终稿确认请求",得到明确"可以生成"后,按 assets/prd-template.md 一次性生成 PRD-MD。
阶段 3 · 测试用例讨论与定稿
1. 先对齐颗粒度:先按 references/test-case-example.md 给用户一条写到底的样板用例(普通原子 1 条 + 任务类 1 条),确认结构/颗粒度,再批量。 2. 按 assets/test-cases-template.md 组织:§0 全局约定 + §0.5 阶段编排(资格地基→连通→能力→复杂长链路,前阶段全过才进下一;安全贯穿)+ 模块分组 + 末尾「别家怎么减」。 3. 逐条原子用例写满 13 字段(见「附录 B」),每条带 代码依据 文件:行。 4. 任务类用例严格按理念 #8:写明确任务名、轮数、每一轮发什么内容(原文)、每一轮期望什么结果;长链路任务的 verbatim prompt 写进该用例「测试数据」字段,含轮数规则(一轮的可观测信号、最少/最多轮、超轮归类 client)与独立复跑防作弊。 5. 用原子点枚举法列全本家用例:{每条链路} × {每个相关行为/能力} × {失败五类适用项},逐项落一条 TC,避免漏。 6. 终稿确认后生成 测试用例-MD。
阶段 4 · 双 HTML(套模板)
- PRD-MD → 套
assets/prd-review.html.tmpl;测试用例-MD → 套assets/test-cases-review.html.tmpl。 - 生成后必须跑
references/html-fill-spec.md的 MD↔HTML 1:1 校验算法;FAIL(任何字段/步骤/整节被删或压缩)不得交付,补齐重校。
阶段 5 · 对抗校核
- 按
references/adversarial-review-prompts.md,开 ≥3 个无共享上下文 sub-agent(代码对账 / 覆盖完整性 / 可执行性+证据诚实性),结构化输出。 - 主 agent 汇总:共识 must-fix(AI 能修的:行号笔误/格式/HTML 压缩 先修);人决策项(代码语义争议/范围/取舍/诚实性 单独暴露,不替用户决)。
- 校核挑不出设计矛盾、只剩笔误,才算这版稳。
阶段 6 · 冻结与版本管理
- 用户确认后,在两份 MD 头部状态行标
Frozen + 日期 + commit。 - 输出可粘贴到项目
docs/PRD_REGISTRY.md的总集行(见「附录 C」)。
四、产物约定(每个 PRD 固定 4 件)
| 产物 | 给谁 | 角色 | 模板 |
|---|---|---|---|
docs/prd/PRD-xxx.md | AI | 需求事实源 | assets/prd-template.md |
docs/prd/PRD-xxx-测试用例.md | AI | 测试用例事实源 | assets/test-cases-template.md |
docs/prd/PRD-xxx-review.html | 人 | PRD 查阅 | assets/prd-review.html.tmpl |
docs/prd/PRD-xxx-测试用例-review.html | 人 | 用例查阅 | assets/test-cases-review.html.tmpl |
(路径以用户/项目既有规范为准;不知道就问。)
附录 A · 术语表(对人输出仍说人话)
| 术语 | 人话 |
|---|---|
| 冻结 | 文档定稿、状态行标 Frozen+日期+commit,之后改范围需重新确认 |
| 门禁 | 准入条件:某组用例全绿才算"通过/可发布",否则拦截 |
| 真Key 证据 | 用真实 API Key 打真实上游跑出来,证"真能用" |
| 抓包证据 | 走假上游(恒回固定值)抓请求,只证"发出去字段对",不证能用 |
| 同义反复/假绿 | 没造出会触发的场景就"没违规所以算过"——虚假通过 |
| BLOCKED-待实现 | 功能/路径未实现,既非通过也非跳过,如实标阻塞 |
附录 B · 测试用例 13 字段(标准)
编号 / 名称(只含一个原子点)/ 所属模块·阶段 / 优先级(P0阻断·P1·P2) / 证据类型(真Key|抓包|不费Key) / 前置条件(逐条) / 测试数据(精确字面值;任务类含任务名+轮数+每轮内容+每轮期望) / 测试步骤(每步=动作→该步预期) / 通过标准(客观二值) / 失败判定与归类(五类) / 后置清理 / 证据产物 / 代码依据(文件:行)。
- 原子性判据:名称或通过标准出现"和/且/+"连接多个独立断言 → 必拆。
- 颗粒度下界(四项静态自检,缺一不合格):① 每步命令含全部环境变量字面值 ② 每步配该步预期 ③ JSON/body 完整可解析、prompt 一字不差原文 ④ 默认值标
文件:行。 - 颗粒度上界:单条用例步骤宜 ≤ 8 步;超出多半没拆原子,回看原子性判据。
- 失败五类:preflight / gateway / provider / client / cleanup,按"失败最早环节"归唯一一类;cleanup 类一票否决。
附录 D · 适用边界与通用映射
- 本 skill 的 §0.5 阶段编排(资格→连通→能力→长链路)与失败五类(preflight/gateway/provider/client/cleanup)最贴合"客户端经网关/服务接上游"类项目。
- 非此类项目(纯前端/算法库/审批流等)的通用映射:阶段 = 静态/单元 → 集成 → 端到端 → 长链路/复杂场景;失败五类 → preflight(环境/依赖缺失)/构建或单元(等价 gateway)/外部依赖(等价 provider)/业务逻辑或交互(等价 client)/清理隔离(cleanup)。按此重命名,结构与判定口径不变。
- 别家减项里"参照断言库规则":指项目内若有"按能力推导该发/不该发字段"的辅助库(如某仓
capability-asserts.js,输入能力声明 → 输出每路径 mustHave/mustNotHave),人写别家用例时参照其规则;无此库时按其等价规则人工推导,不依赖该库存在。 - 运行环境与降级:阶段 5 对抗校核优先开 ≥3 个独立 sub-agent(不传本会话历史);若环境无 sub-agent 能力,降级为"串行 3 轮独立审、每轮显式声明视角且不复用上一轮结论",并如实标注"非真并行",禁止假装开了 3 个 agent(这本身就是 skill 反对的假绿)。
- 产物路径与 PRD-ID:默认
docs/prd/PRD-<NNN>.md,编号取项目docs/PRD_REGISTRY.md现有最大号+1(查重);项目已有规范以其为准;都不确定时问用户一次,不默认乱编。
附录 C · PRD 总集(台账)
写完后维护项目仓库 docs/PRD_REGISTRY.md(每个 PRD 一行,永远指向最新链接,历史交给 Git)。需用户确认:版本号、PRD 链接、(可选)总集路径。输出单行: | <版本> | <标题> | <需求内容详细摘要 3-8 句> | <PRD链接> |(四字段内不得含 |)。 references/prd-registry-demo.md 仅示例。
<!DOCTYPE html>
<!--
PRD HTML 查阅模板(套用:从 PRD-MD 抽内容灌占位符,禁手写结构/改样式)
占位符:{{PRD_ID}} {{TITLE}} {{STATUS}} {{TOC}} {{OVERVIEW}} {{JOURNEY}} {{MERMAID}} {{US_CARDS}}
生成后必须跑 references/html-fill-spec.md 的 1:1 校验,FAIL 不交付。
规则:与 PRD-MD 严格 1:1,不删故事子项、不丢 Mermaid 代码块原文、不压缩整节。
★ 标题文本必须是单一文本节点、与 MD 一字不差(不得把序号拆进 <span>,否则去标签后 1:1 失配)。
★ 视觉层级只靠 CSS(h2 左色条、h3 前缀、卡片分区头),不改任何可见文字。
Mermaid:用 .diagram 容器,<pre class="mermaid"> 装渲染副本,再用 <details><pre> 留一份源码原文
(源码副本 verbatim,1:1 校验只扫这份;渲染失败也有源码可看)。
-->
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{PRD_ID}} · {{TITLE}}</title>
<style>
:root{
--bg:#f5f6f8;--card:#fff;--ink:#1d2129;--soft:#646a73;--mute:#8a9099;
--line:#e6e8eb;--line2:#eef0f2;
--accent:#3370ff;--accent-d:#1d4fd8;--accent-soft:#eef3ff;--code:#f3f4f6;
--ok:#1f8a5b;--ok-soft:#eaf6f0;--ok-line:#bfe3cf;
}
*{box-sizing:border-box}html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
.shell{max-width:1200px;margin:0 auto;padding:32px 24px 80px;display:flex;gap:28px;align-items:flex-start}
/* ---- 目录:树形父子缩进 ---- */
.toc{position:sticky;top:32px;flex:0 0 244px;max-height:calc(100vh - 64px);overflow:auto;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 10px;font-size:13px}
.toc .t{font-weight:700;color:var(--mute);margin:2px 10px 12px;font-size:11px;letter-spacing:1.5px}
.toc a{display:block;color:#3c4148;text-decoration:none;padding:6px 10px;border-radius:8px;margin:1px 0;border-left:2px solid transparent;transition:background .12s,color .12s}
.toc a:hover{background:#f1f2f4;color:var(--ink)}
.toc a.active{background:var(--accent-soft);color:var(--accent-d);font-weight:600;border-left-color:var(--accent)}
.toc a.lv1{font-weight:600;color:var(--ink);margin-top:6px}
.toc a.lv2{padding-left:24px;color:#5c626a;font-size:12.5px}
.toc .grp{font-size:11px;font-weight:700;color:var(--mute);letter-spacing:.5px;margin:14px 10px 4px}
/* ---- 主体 ---- */
.wrap{flex:1 1 auto;min-width:0;max-width:880px;background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,.04);padding:38px 46px 60px}
@media(max-width:1080px){.shell{flex-direction:column;padding:16px 14px 60px}.toc{position:sticky;top:0;flex:none;width:100%;max-height:none;display:flex;flex-wrap:wrap;gap:2px;z-index:5}.toc .t,.toc .grp{width:100%}.toc a,.toc a.lv2{border-left:none;padding-left:10px}.wrap{max-width:none;padding:24px 18px 40px}}
h1{font-size:24px;margin:0 0 6px;letter-spacing:-.3px}
.sub{color:var(--soft);font-size:13px;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid var(--line)}
h2{font-size:18px;margin:42px 0 14px;padding:2px 0 2px 14px;border-left:4px solid var(--accent);scroll-margin-top:24px;line-height:1.4}
h3{font-size:13.5px;font-weight:700;color:var(--soft);margin:24px 0 8px;letter-spacing:.2px}
h3:before{content:"▸";color:var(--accent);margin-right:7px}
p{margin:8px 0}
code{background:var(--code);border-radius:5px;padding:1.5px 5px;font:12.5px ui-monospace,Menlo,monospace;word-break:break-word}
pre{background:#f7f8fa;color:#1d2129;border:1px solid var(--line);border-radius:10px;padding:13px 15px;font:12.5px/1.65 ui-monospace,Menlo,monospace;white-space:pre-wrap;margin:8px 0;overflow:auto}
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13.5px}
th,td{border:1px solid var(--line);padding:8px 11px;text-align:left;vertical-align:top}th{background:#f5f6f8;font-weight:600}
ul,ol{margin:6px 0;padding-left:22px}li{margin:5px 0}ol ol,ul ul,ol ul,ul ol{margin:4px 0}
/* ---- 字段块(标签即锚点)---- */
.fld{margin:16px 20px}
.fk{display:inline-block;font-size:12px;font-weight:700;color:var(--accent-d);background:var(--accent-soft);border-radius:6px;padding:2px 9px;margin-bottom:8px;letter-spacing:.3px}
.fv{font-size:14.5px}
/* ---- 用户故事卡:卡头 / 分区字段 ---- */
.us{background:#fff;border:1px solid var(--line);border-radius:14px;padding:0;margin:22px 0;scroll-margin-top:24px;overflow:hidden}
.ush{display:flex;align-items:baseline;gap:11px;background:linear-gradient(180deg,#f4f7fd,#fbfcfe);border-bottom:1px solid var(--line);padding:15px 20px}
.usid{font:800 13px ui-monospace,Menlo,monospace;color:#fff;background:var(--accent);border-radius:6px;padding:3px 9px;flex:none;align-self:flex-start}
.ustitle{font-size:15px;font-weight:700;line-height:1.5}
.us .fld:first-of-type{margin-top:18px}
.fld.wf pre{background:#fbfcfd}
.fld.acc .fv strong{color:var(--accent-d)}
/* ---- Mermaid ---- */
.diagram{background:#fbfcfd;border:1px solid var(--line);border-radius:12px;padding:18px;margin:14px 0;overflow:auto}
.diagram .mermaid{background:none;border:0;padding:0;margin:0;color:inherit;white-space:normal;display:flex;justify-content:center;text-align:center;min-height:32px}
.diagram .mermaid svg{max-width:100%;height:auto}
.diagram details{margin-top:10px}
.diagram summary{cursor:pointer;color:var(--soft);font-size:12px;user-select:none}
.diagram details pre{margin-top:8px}
.foot{color:var(--mute);font-size:12.5px;margin-top:42px;border-top:1px solid var(--line);padding-top:16px}
</style></head><body>
<div class="shell">
<nav class="toc"><div class="t">目录</div>{{TOC}}</nav>
<main class="wrap">
<h1>{{PRD_ID}} · {{TITLE}}</h1>
<div class="sub">{{STATUS}} · 本页与 PRD-MD 严格 1:1,不压缩 · 配套:{{PRD_ID}}-测试用例</div>
<h2 id="overview">1. 综述</h2>
{{OVERVIEW}}
<h2 id="journey">1.2 用户旅程地图</h2>
{{JOURNEY}}
<h2 id="mermaid">1.3 流程/状态/时序图</h2>
{{MERMAID}}
<h2 id="stories">2. 用户故事</h2>
{{US_CARDS}}
<div class="foot">本页给人查阅,全部内容来自 PRD-MD,仅翻译排版、未增删事实。Mermaid 图浏览器内渲染,源码原文在「查看 Mermaid 源码」内完整保留。</div>
</main></div>
<script>
(function(){
var links=[].slice.call(document.querySelectorAll('.toc a[href^="#"]'));
var map=links.map(function(a){return{a:a,el:document.getElementById(a.getAttribute('href').slice(1))};}).filter(function(x){return x.el;});
function onScroll(){
var cur=null;
for(var i=0;i<map.length;i++){ if(map[i].el.getBoundingClientRect().top<=140) cur=map[i]; else break; }
if(!cur&&map.length) cur=map[0];
links.forEach(function(a){a.classList.remove('active');});
if(cur) cur.a.classList.add('active');
}
window.addEventListener('scroll',onScroll,{passive:true});
window.addEventListener('resize',onScroll); onScroll();
})();
</script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({startOnLoad:true,theme:'default',securityLevel:'loose',flowchart:{useMaxWidth:true,htmlLabels:true},sequence:{useMaxWidth:true},themeVariables:{fontFamily:'-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif'}});
</script>
</body></html>
<!--
===== 片段格式(生成时灌入,勿改结构/勿改可见文字)=====
TOC(树形:父项 lv1,子项 lv2):
<a class="lv1" href="#overview">1 综述</a>
<a class="lv2" href="#journey">1.2 用户旅程地图</a>
<a class="lv2" href="#mermaid">1.3 流程/状态/时序图</a>
<a class="lv1" href="#stories">2 用户故事</a>
<a class="lv2" href="#us-01">US-01 …</a>
Mermaid(§1.3 每个图):
<h3>1.3.1 …</h3>
<figure class="diagram">
<pre class="mermaid">渲染副本:mermaid-safe(flowchart 节点文字一律包 ["…"],文字内 ASCII 双引号 " 写成 #quot;)</pre>
<details><summary>查看 Mermaid 源码</summary><pre>源码副本:与 PRD-MD 一字不差的原文(1:1 校验只认这份;不做任何转义)</pre></details>
</figure>
US 卡片(卡头一句话标题;故事所有子项全保留;视觉层级靠 class,文字不动):
<div class="us" id="us-01">
<div class="ush"><span class="usid">US-01</span><div class="ustitle">作为…我希望…以便…(与 MD 故事标题一字不差)</div></div>
<div class="fld"><div class="fk">价值陈述</div><div class="fv">…</div></div>
<div class="fld"><div class="fk">业务规则与逻辑</div><div class="fv">前置/Happy Path/异常处理 全列…</div></div>
<div class="fld acc"><div class="fk">验收标准</div><div class="fv">GIVEN-WHEN-THEN 每个场景…</div></div>
<div class="fld wf"><div class="fk">页面布局线框图</div><pre>ASCII 原文</pre></div> <!-- UI 故事必有 -->
</div>
-->
# 产品需求文档:[项目/功能名称] - V[版本号]
> 状态:Draft | Frozen(<日期> <commit>)
> 事实源:本 MD(给 AI);查阅视图:`PRD-xxx-review.html`(套模板生成,与本 MD 严格 1:1,不压缩)
> 配套:`PRD-xxx-测试用例.md`(同步产出)
## 1. 综述 (Overview)
### 1.1 项目背景与核心问题
(此处填写经你引导和用户确认的,对顶层问题的清晰描述,提供全局上下文)
### 1.2 核心业务流程 / 用户旅程地图
(此处填写经你引导和用户最终确认的、分阶段的业务流程或用户旅程,作为整个文档的目录和主线)
1. **阶段一:[名称]** - [一句话描述该阶段的用户目标]
2. **阶段二:[名称]** - [一句话描述该阶段的用户目标]
...
### 1.3 Mermaid 图(流程/状态/时序)
> 说明:Mermaid 图用于“需求对齐”,避免歧义;避免写成技术实现细节(不要写 API 路径、字段、HTTP code、框架/库)。
#### 1.3.1 用户操作流(必填)flowchart TD A[开始:用户进入/触发] --> B[用户操作] B --> C{关键判断/分支} C -->|成功| D[系统反馈:成功态/跳转] C -->|失败/异常| E[系统反馈:错误提示/可恢复动作] D --> F[结束] E --> F
#### 1.3.2 状态机(当存在明确状态流转对象时必填)stateDiagram-v2 [] --> 草稿 草稿 --> 已提交: 提交 已提交 --> 处理中: 开始处理 处理中 --> 成功: 完成 处理中 --> 失败: 失败 失败 --> 已提交: 重试 成功 --> []
#### 1.3.3 关键场景时序(仅当“时序/并发/重试/超时”影响用户可见结果时填写)sequenceDiagram participant U as 用户 participant A as App/前台 participant S as 系统
U->>A: 发起关键操作 A->>S: 请求处理(抽象描述) alt 成功 S-->>A: 返回成功结果/状态 A-->>U: 展示成功反馈(文案/状态) else 失败/异常 S-->>A: 返回失败原因(用户可理解) A-->>U: 展示错误提示 + 可恢复操作 end
## 2. 用户故事详述 (User Stories)
### 阶段一:[阶段名称]
---
#### **US-[编号]: [用户故事标题,格式:作为...我希望...以便于...]**
* **价值陈述 (Value Statement)**:
* **作为** [用户角色]
* **我希望** [完成某项操作/达到某个目的]
* **以便于** [实现某种价值/解决某个问题]
* **业务规则与逻辑 (Business Logic)**:
1. **前置条件**: (执行此功能需要满足的前提)
2. **操作流程 (Happy Path)**: (一步步描述用户成功路径下的操作与系统反馈)
3. **异常处理 (Error Handling)**: (详细罗列各种可能出错的情况、降级/补偿策略以及对应的系统行为)
* **验收标准 (Acceptance Criteria)**: (使用 GIVEN-WHEN-THEN 格式,为核心场景提供清晰的验收条件,至少覆盖成功与失败/异常路径)
* **场景1: [场景名]**
* **GIVEN** [上下文/前置条件]
* **WHEN** [用户执行的动作]
* **THEN** [期望看到的系统结果]
* **场景2: ...**
---
* **页面布局线框图 (ASCII Wireframe)**: <!-- 对于涉及UI的故事,此项必填 -->(此处插入经用户最终确认的ASCII线框图)
---
(下一个用户故事...)
## 3. 发布门禁与实现状态 (Release Gate & Implementation Status)
> 承接:测试用例文档只写"该怎样",不评"代码现在能不能跑";代码实现/发布状态记在这里。
- **实现状态**:未开始 | 开发中 | 代码完成 | 已发布(按需逐故事或整体标)
- **发布门禁**:列出"准入条件"——哪组测试用例(引用 `PRD-xxx-测试用例.md` 的 TC 区间)全绿、哪些真 Key 用例必过,才允许发布;未达门禁如实标 BLOCKED,不得伪装通过。
- **机器可读认证结果(可选但推荐)**:若需拦截"未通过却被标可用",产出认证结果文件(如 cert json:每 TC 过/不过 + 证据)并据此写状态位(如 registry/能力矩阵 ✅/🟡/❌)。<!DOCTYPE html>
<!--
测试用例 HTML 查阅模板(套用:从 测试用例-MD 抽内容灌占位符,禁手写结构/改样式)
占位符:{{PRD_ID}} {{STATUS}} {{TOC}} {{SEC0}} {{PHASE_TABLE}} {{TC_CARDS}} {{ELSEWHERE}}
生成后必须跑 references/html-fill-spec.md 的 1:1 校验,FAIL 不交付。
规则:与 MD 严格 1:1,不删字段/不删步骤/不压缩整节。
★ 13 字段全保留,但按「① 用例身份与前提 ② 如何执行 ③ 如何判定」三组归拢,组内才是字段。
★ 模块/优先级/证据类型放在卡头 + 身份组的 .idgrid 里(均为可见文本,1:1 仍命中),
不再单独平铺成三个整宽 .fld(去掉重复堆叠)。证据类型若是长句(如 BLOCKED 说明),
其全文必须落在 .idgrid 的 <dd> 里,不能只剩 badge。
★ 视觉层级只靠 CSS(分区头、步骤序号、通过/失败色块),不改任何可见文字。
-->
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{PRD_ID}} 测试用例</title>
<style>
:root{
--bg:#f5f6f8;--card:#fff;--ink:#1d2129;--soft:#646a73;--mute:#8a9099;
--line:#e6e8eb;--line2:#eef0f2;
--accent:#3370ff;--accent-d:#1d4fd8;--accent-soft:#eef3ff;--code:#f3f4f6;
--g2:#6b4ef0;--g3:#1f8a5b;
--ok:#1f8a5b;--ok-soft:#eaf6f0;--ok-line:#bfe3cf;
--warn:#b4541f;--warn-soft:#fdf1ea;--warn-line:#f0d2bd;
--p0:#a23b2e;--p1:#8a5e12;--p2:#5b6168;--real:#1f8a5b;--cap:#8a5e12;--blk:#5b6168;
}
*{box-sizing:border-box}html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
.shell{max-width:1200px;margin:0 auto;padding:32px 24px 80px;display:flex;gap:28px;align-items:flex-start}
/* ---- 目录:树形父子缩进 ---- */
.toc{position:sticky;top:32px;flex:0 0 244px;max-height:calc(100vh - 64px);overflow:auto;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 10px;font-size:13px}
.toc .t{font-weight:700;color:var(--mute);margin:2px 10px 12px;font-size:11px;letter-spacing:1.5px}
.toc a{display:block;color:#3c4148;text-decoration:none;padding:6px 10px;border-radius:8px;margin:1px 0;border-left:2px solid transparent;transition:background .12s,color .12s}
.toc a:hover{background:#f1f2f4;color:var(--ink)}
.toc a.active{background:var(--accent-soft);color:var(--accent-d);font-weight:600;border-left-color:var(--accent)}
.toc a.lv1{font-weight:600;color:var(--ink);margin-top:6px}
.toc a.lv2{padding-left:24px;color:#5c626a;font-size:12.5px}
.toc .grp{font-size:11px;font-weight:700;color:var(--mute);letter-spacing:.5px;margin:14px 10px 4px}
/* ---- 主体 ---- */
.wrap{flex:1 1 auto;min-width:0;max-width:900px;background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,.04);padding:38px 46px 60px}
@media(max-width:1080px){.shell{flex-direction:column;padding:16px 14px 60px}.toc{position:sticky;top:0;flex:none;width:100%;max-height:none;display:flex;flex-wrap:wrap;gap:2px;z-index:5}.toc .t,.toc .grp{width:100%}.toc a,.toc a.lv2{border-left:none;padding-left:10px}.wrap{max-width:none;padding:24px 18px 40px}}
h1{font-size:24px;margin:0 0 6px;letter-spacing:-.3px}
.sub{color:var(--soft);font-size:13px;margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--line)}
h2{font-size:18px;margin:42px 0 14px;padding:2px 0 2px 14px;border-left:4px solid var(--accent);scroll-margin-top:24px;line-height:1.4}
h2.mod{border-left-color:var(--g2)}
code{background:var(--code);border-radius:5px;padding:1.5px 5px;font:12px ui-monospace,Menlo,monospace;word-break:break-word}
pre{background:#f7f8fa;color:#1d2129;border:1px solid var(--line);border-radius:10px;padding:12px 14px;font:12px/1.65 ui-monospace,Menlo,monospace;white-space:pre-wrap;margin:6px 0;overflow:auto}
table{width:100%;border-collapse:collapse;margin:10px 0;font-size:13px}
th,td{border:1px solid var(--line);padding:7px 9px;text-align:left;vertical-align:top}th{background:#f5f6f8;font-weight:600}
ul,ol{margin:6px 0;padding-left:22px}li{margin:5px 0}
.legend{margin:14px 0 0;font-size:13px}
/* ---- TC 卡:卡头 ---- */
.tc{background:#fff;border:1px solid var(--line);border-radius:14px;padding:0;margin:22px 0;scroll-margin-top:24px;overflow:hidden}
.tch{display:flex;gap:12px;background:linear-gradient(180deg,#f4f7fd,#fbfcfe);border-bottom:1px solid var(--line);padding:15px 20px}
.tcid{font:800 13px ui-monospace,Menlo,monospace;color:#fff;background:var(--accent);border-radius:6px;padding:3px 9px;height:fit-content;flex:none}
.tch-main{min-width:0}
.tct{font-size:15.5px;font-weight:700;line-height:1.45;margin-bottom:8px}
.tcmeta{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.tcmod{font-size:12px;color:var(--soft)}
.badge{font-size:11px;font-weight:700;border-radius:5px;padding:2px 8px;border:1px solid}
.b-p0{color:#fff;background:var(--p0);border-color:var(--p0)}
.b-p1{color:var(--p1);background:#fbf3df;border-color:#e6cf9b}
.b-p2{color:var(--p2);background:#eef0f2;border-color:#d3d7dc}
.b-real{color:#fff;background:var(--real);border-color:var(--real)}
.b-cap{color:var(--cap);background:#fbf3df;border-color:#e6cf9b}
.b-blk{color:#fff;background:var(--blk);border-color:var(--blk)}
/* ---- TC 卡:三个分区 ---- */
.grp{border-top:1px solid var(--line2)}
.grp:first-of-type{border-top:0}
.gh{display:flex;align-items:center;gap:9px;font-size:12px;font-weight:700;color:var(--accent-d);letter-spacing:.5px;padding:14px 20px 2px}
.gn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:var(--accent);color:#fff;font:700 11px ui-monospace,Menlo,monospace}
.grp.g-exec .gn{background:var(--g2)}.grp.g-exec .gh{color:var(--g2)}
.grp.g-judge .gn{background:var(--g3)}.grp.g-judge .gh{color:var(--g3)}
.grp .fld{margin:12px 20px 16px}
/* 身份组:紧凑键值网格,不再整宽平铺 */
.idgrid{display:grid;grid-template-columns:max-content 1fr;gap:7px 18px;font-size:13.5px;margin:12px 20px 16px}
.idgrid dt{color:var(--soft);font-weight:600}
.idgrid dd{margin:0}
/* 字段标签 */
.fld{margin:14px 20px}
.fk{display:inline-block;font-size:12px;font-weight:700;color:var(--accent-d);background:var(--accent-soft);border-radius:6px;padding:2px 9px;margin-bottom:8px;letter-spacing:.3px}
.fv{font-size:14px}
/* 测试步骤:序号圆点 + 预期独立色块 */
ol.steps{list-style:none;margin:6px 0;padding:0;counter-reset:st}
ol.steps>li{position:relative;counter-increment:st;padding:10px 0 10px 34px;border-bottom:1px dashed var(--line2)}
ol.steps>li:last-child{border-bottom:0}
ol.steps>li:before{content:counter(st);position:absolute;left:0;top:8px;width:22px;height:22px;border-radius:50%;background:var(--accent-soft);color:var(--accent-d);font:700 12px ui-monospace,Menlo,monospace;display:flex;align-items:center;justify-content:center}
.exp{display:block;margin-top:7px;padding:7px 11px;background:var(--ok-soft);border-left:3px solid var(--ok);border-radius:0 6px 6px 0;color:#1c5c3e;font-size:13px}
.exp:before{content:"预期 → ";font-weight:700;color:var(--ok)}
/* 通过 / 失败:高亮色块,跳出来 */
.pass-box{background:var(--ok-soft);border:1px solid var(--ok-line);border-left:4px solid var(--ok);border-radius:8px;padding:11px 14px;font-size:14px;color:#1c5c3e;font-weight:600}
.fail-box{background:var(--warn-soft);border:1px solid var(--warn-line);border-left:4px solid var(--warn);border-radius:8px;padding:9px 14px}
.fail-box ul{margin:4px 0}
ul.tight{margin:4px 0;padding-left:20px}ul.tight>li{margin:4px 0}
.dep{font-size:12px;color:var(--mute);background:#fafbfc;border-top:1px solid var(--line2);padding:11px 20px;font-family:ui-monospace,Menlo,monospace;word-break:break-word}
.foot{color:var(--mute);font-size:12.5px;margin-top:42px;border-top:1px solid var(--line);padding-top:16px}
</style></head><body>
<div class="shell">
<nav class="toc"><div class="t">目录</div>{{TOC}}</nav>
<main class="wrap">
<h1>{{PRD_ID}} 测试用例</h1>
<div class="sub">{{STATUS}} · 本页与 测试用例-MD 严格 1:1,不压缩 · 一条用例=一个原子验证点</div>
<table class="legend"><tr><th>标记</th><th>含义</th></tr>
<tr><td><span class="badge b-p0">P0</span></td><td>阻断级,不过则不通过</td></tr>
<tr><td><span class="badge b-real">真Key</span></td><td>打真实上游,证"真能用"</td></tr>
<tr><td><span class="badge b-cap">抓包</span></td><td>假上游,只证"字段对",不证能用</td></tr>
<tr><td><span class="badge b-blk">BLOCKED</span></td><td>未实现,非通过非跳过</td></tr></table>
<h2 id="sec0">0 全局约定</h2>
{{SEC0}}
<h2 id="phase">0.5 测试阶段编排</h2>
{{PHASE_TABLE}}
{{TC_CARDS}}
{{ELSEWHERE}}
<div class="foot">本页给人查阅,全部内容来自 测试用例-MD(每条带代码依据 文件:行),仅翻译排版、未增删事实。一条用例=一个原子验证点,13 字段,每步动作配预期。</div>
</main></div>
<script>
(function(){
var links=[].slice.call(document.querySelectorAll('.toc a[href^="#"]'));
var map=links.map(function(a){return{a:a,el:document.getElementById(a.getAttribute('href').slice(1))};}).filter(function(x){return x.el;});
function onScroll(){
var cur=null;
for(var i=0;i<map.length;i++){ if(map[i].el.getBoundingClientRect().top<=140) cur=map[i]; else break; }
if(!cur&&map.length) cur=map[0];
links.forEach(function(a){a.classList.remove('active');});
if(cur) cur.a.classList.add('active');
}
window.addEventListener('scroll',onScroll,{passive:true});
window.addEventListener('resize',onScroll); onScroll();
})();
</script>
</body></html>
<!--
===== 片段格式(生成时灌入,勿改结构/勿改可见文字)=====
TOC(树形:模块 lv1,TC lv2;§0/§0.5/别家 lv1):
<a class="lv1" href="#sec0">0 全局约定</a>
<a class="lv1" href="#phase">0.5 测试阶段编排</a>
<div class="grp">测试模块</div>
<a class="lv1" href="#m-b">模块 B · 真 Key 基础连通</a>
<a class="lv2" href="#tc-003">TC-003 网关健康接口</a>
<a class="lv1" href="#elsewhere">别家怎么减</a>
模块标题(每模块前插一条): <h2 class="mod" id="m-b">模块 B · …</h2>
TC 卡片(13 字段全保留,按三组归拢;卡头与身份组均为可见文本):
<div class="tc" id="tc-013">
<div class="tch"><span class="tcid">TC-013</span>
<div class="tch-main">
<div class="tct">用例名称(与 MD 一字不差)</div>
<div class="tcmeta"><span class="tcmod">模块D / 阶段3</span>
<span class="badge b-p0">P0</span><span class="badge b-real">真Key</span></div>
</div></div>
<div class="grp">
<div class="gh"><span class="gn">1</span>用例身份与前提</div>
<dl class="idgrid">
<dt>所属模块/阶段</dt><dd>模块D 真 Key Codex 闭环 / 阶段3 复杂长链路</dd>
<dt>优先级</dt><dd>P0(与 MD 字面值一致;长说明全文照搬)</dd>
<dt>证据类型</dt><dd>真Key(若为 BLOCKED 等长句,整句全文放这里,勿截断)</dd>
</dl>
<div class="fld"><div class="fk">前置条件</div><div class="fv"><ul class="tight"><li>…</li></ul></div></div>
</div>
<div class="grp g-exec">
<div class="gh"><span class="gn">2</span>如何执行</div>
<div class="fld"><div class="fk">测试数据(精确字面值)</div><div class="fv">…;prompt/JSON 用 <pre>原文</pre> 不截断;逐轮表用 <table></div></div>
<div class="fld"><div class="fk">测试步骤</div><div class="fv"><ol class="steps"><li>动作:…<span class="exp">…</span></li></ol></div></div>
</div>
<div class="grp g-judge">
<div class="gh"><span class="gn">3</span>如何判定</div>
<div class="fld"><div class="fk">通过标准</div><div class="fv"><div class="pass-box">…</div></div></div>
<div class="fld"><div class="fk">失败判定与归类</div><div class="fv"><div class="fail-box"><ul class="tight"><li>…</li></ul></div></div></div>
<div class="fld"><div class="fk">后置/清理</div><div class="fv">…</div></div>
<div class="fld"><div class="fk">证据产物</div><div class="fv">…</div></div>
</div>
<div class="dep">代码依据:文件:行(原样)</div>
</div>
-->
<PRD-ID> 测试用例(原子用例)
状态:Draft | Frozen(<日期> <commit>)
事实源:本 MD(给 AI);查阅视图:<PRD-ID>-测试用例-review.html(套模板,与本 MD 严格 1:1,不压缩)一条用例 = 一个原子验证点。每条带「代码依据 文件:行」,断言全部从真实代码读出。
---
0. 全局约定(每条用例都隐含,写一次)
- 工作目录 / 命令前缀:<填:所有命令在哪个目录跑>
- `<真key>` 等占位:<填:真实凭据怎么传,环境变量名>
- 环境隔离:<填:临时目录/隔离方式,不碰真实配置;代码依据 文件:行>
- 两类证据:
真Key=打真实上游证"真能用";抓包=假上游恒回固定值只证"字段对",不证能用。 - 门禁句(必写):发「<本 PRD> 通过」必须全部真 Key 阻断用例(<列出具体 TC 区间>)全绿;capture-only 不发通过。
- 失败统一五类:preflight / gateway / provider / client / cleanup(按失败最早环节归唯一一类;cleanup 一票否决)。
0.5 测试阶段编排(执行顺序与门禁 · 前阶段全过才进下一阶段)
| 阶段 | 测什么点 | 包含用例 | 门禁 |
|---|---|---|---|
| 阶段0 资格地基(不费Key) | <填:底层/边界> | TC-0xx… | 不过 → 全流程停 |
| 阶段1 连通(先证明能连) | <填:直连+客户端最简连通> | TC-0xx… | 全过 → 进阶段2,否则停 |
| 阶段2 能力(简单任务测开关/切换) | <填:思考/强度/工具/换模型> | TC-0xx… | 全过 → 进阶段3 |
| 阶段3 复杂长链路 | <填:明确长任务,见任务类用例> | TC-0xx(各客户端各一)… | 全过 → 通过 |
| 贯穿·安全 | 配置不被改/不泄密/清理 | TC-0xx… | 任一不过 → 对应用例一票否决 |
---
模块 A · <模块名>
TC-001 <一句话名称,只含一个原子点>
- 用例编号:TC-001
- 用例名称:<只含一个被验证的原子点;出现"和/且/+"必拆>
- 所属模块/阶段:模块 A / 阶段 X
- 优先级:P0 阻断 | P1 | P2
- 证据类型:真Key | 抓包 | 不费Key
- 前置条件:
1. <逐条:环境、是否要真 Key、依赖哪条/哪阶段先过>
- 测试数据(精确字面值):
- 完整命令(含每个环境变量字面值):
<…> - 完整 JSON/body(可被 JSON.parse)/ prompt 一字不差原文:
<…> - 用到的默认值及其出处:
<值>(文件:行) - 测试步骤(每步:动作 → 该步预期):
1. 动作:<精确命令/操作> → 预期:<精确字段值/退出码/日志行> 2. 动作:<…> → 预期:<…>
- 通过标准:<客观二值,满足=过>
- 失败判定与归类:<现象 → 五类之一 + 定位线索>
- 后置/清理:<临时物如何清>
- 证据产物:<报告/JSONL/日志 路径>
- 代码依据:
文件:行(断言来自此处真实代码)
(…逐条原子用例,按 {每条链路} × {每个相关能力/行为} × {失败五类适用项} 枚举展开,不漏…)
---
模块 <X> · 复杂长链路(任务类用例 · 强制明确任务,禁泛化)
任务类用例禁止写「测一个长任务」这种泛描述。必须写死:任务名、跑几轮、每一轮发什么内容(原文)、每一轮期望什么结果。Claude/Codex 等多客户端各写一条同题用例以便对比。
TC-0NN <客户端>-long 复杂长链路(跑 <任务名>)
- 用例编号:TC-0NN
- 用例名称:真实 <客户端> 经 <链路> 完成 <任务名>,在 <M> 轮内自测自修到 <成功标记>
- 所属模块/阶段:模块 X / 阶段3 复杂长链路
- 优先级:P0 | 证据类型:真Key
- 前置条件:
1. 阶段1、阶段2 全过 2. <真实凭据 + 客户端可执行(preflight 通过)>
- 测试数据(精确字面值):
- 任务名称:<任务名,如 TASK-LONG-TODO> —— <一句话任务实质>
- 任务类型:真实多步骤 agent 任务(看懂需求→多步执行→自验→自修→直到成功)
- 覆盖周期:约 <最少>~<最多> 轮、约 <x~y> 分钟
- 轮数规则(写死):一轮 = <可观测信号,如网关日志一条 XXX_DONE,runner grep 计数>;最少 <N> 轮、最多 <M> 轮封顶;超 <M> 轮未收敛 = 失败,归类 client(模型未在时限收敛),与接入失败分开
- 每一轮发什么 / 期望什么(逐轮写死,禁泛化):
| 轮次 | 发给模型的内容(原文/触发条件) | 期望结果(可观测) |
|---|---|---|
| 第 1 轮 | <verbatim prompt 原文,一字不差> | <如:模型创建 todo.js,发起写文件工具调用> |
| 第 2 轮 | <本轮触发/追加内容原文> | <如:模型写 test.js,含断言 N 条> |
| 第 3 轮 | <…> | <如:模型运行 node test.js> |
| … | <…> | <…> |
| 最后一轮 | <…/收敛指令> | 输出含精确行 <成功标记,如 ALL TESTS PASS> |
- 完整 verbatim prompt(首轮原文,照抄发送):
<任务 prompt 一字不差全文>- 测试步骤(每步:动作 → 该步预期):
1. 动作:<起隔离环境 + 发首轮 prompt 命令含全部 env> → 预期:客户端发起,进入多轮 2. 动作:等模型多轮执行 → 预期:网关日志出现 ≥<N> 次 <轮信号> 且 ≤<M> 次 3. 动作:等结束或超时 → 预期:<超时上限> 内结束 4. 动作:runner 读产物 → 预期:<产物文件齐全>;模型自跑输出含 <成功标记> 5. 动作:runner 独立复跑校验(模型猜不到会复跑,防 echo 作弊) → 预期:复跑退出码 0 且含 <成功标记> 6. 动作:grep 本次日志 → 预期:无本次 <失败标记>
- 通过标准:步骤2轮数∈[N,M] 且 步骤3未超时 且 步骤4产物+成功标记 且 步骤5独立复跑也成功 且 步骤6无失败标记
- 失败判定与归类:超轮/超时/独立复跑不过 → client(模型未收敛/谎报);产物缺但有成功文本 → client(echo 作弊,被步骤5抓出);有失败标记 → gateway/provider;客户端 target 未实现 → BLOCKED-待实现(不得标 PASS/SKIPPED)
- 后置/清理:随 run 末尾删临时根目录
- 证据产物:报告 + redact 日志切片(含轮信号计数)+ 产物快照(不含 key)
- 代码依据:<轮信号发出点 文件:行>;<target 追加 文件:行>;任务定义见本用例测试数据
---
别家怎么减(本家最全样板;别家同 N 条结构按真实 capabilities 人工减/换)
| 别家差异(依据 <provider 定义 文件:行>) | 受影响用例及如何减/换 |
|---|---|
| <某 capability=false> | <TCxx 改 mustNotHave / 删 / 标 SKIPPED> |
| <某客户端路径 null> | <对应 TC 标 SKIPPED;未实现标 BLOCKED 不得 PASS> |
标准化实质:结构、13 字段、命令骨架、判定口径对所有家一致;别家在本家这套上按其真实 capabilities 人工减/换字段断言(参照断言库规则,非自动框架)。
对抗校核 sub-agent 提示词(阶段 5 用)
每完成一版 PRD/测试用例双文档,必须开 ≥3 个无共享上下文的对抗 sub-agent 独立校核。自验通过 ≠ 可推进;挑不出设计矛盾、只剩笔误才算稳。
编排规则
- 数量:≥3 个,下面 3 个角色各 1 个,可按需加(如协议语义、安全)。
- 无共享上下文:每个用独立 Task 启动,不传本会话历史;只给「主审文件路径 + 对照文件路径 + 角色指令 + 输出 schema」。
- 对抗姿态:每个 prompt 都要求 agent 默认文档有问题、去证明,禁止附和。
- 汇总:主 agent 收齐 3 份结构化结果 →
- 共识 must-fix 且
谁修=AI(行号笔误/格式/HTML 压缩/字段缺失)→ 主 agent 直接修,重出,重校。 谁修=人(代码语义争议/范围/取舍/证据归属)→ 单独列给用户决,不替用户决。
统一输出 schema(每条发现一行)
{ 定位: 文件/TC编号/章节, 角度: 1|2|3, 结论: PASS|FAIL, 证据: <文件:行 或 原文>, 是否must-fix: y/n, 谁修: AI|人 }末尾给:必补清单(按严重度)+ 一句话总判。
---
角色 1 · 代码对账审计
你是代码对账审计员。任务很窄:核对测试用例文档里每一处「代码依据 文件:行」是不是真的那么写。不评设计、不提改进,只查"引用与代码是否一致",默认有错去证明,不附和。
主审:<测试用例-MD 路径>
被引用代码根目录:<代码仓路径>
逐条核对每个「代码依据」标注 + 关键断言(健康字段/注入字段/默认值/命令与env名)是否与该文件该行真实一致:属实 / 行号偏差(实际第几行) / 内容不符(实际是什么) / 该文件根本无此逻辑。
特别查:断言里出现的字段能否在代码 grep 到(grep 不到=自创字段=必删)。
按统一 schema 输出 + 必须改的对账错误清单。只对账,不要套话。角色 2 · 覆盖完整性审计
你是覆盖完整性审计员。默认"该测的有遗漏、有名义覆盖实际没测",去证明,不附和。
主审:<测试用例-MD 路径>
对照:<PRD-MD 路径>、<相关代码/历史覆盖记录路径>
1. PRD 每个用户故事/验收标准/异常路径,是否都有对应原子用例?逐条核对,列出无覆盖或覆盖含糊的。
2. 原子点枚举({链路}×{能力/行为}×{失败五类适用项})有没有漏组合?
3. 有没有"用例名义覆盖某点,但其通过标准实际没验它"(名义覆盖)?
4. 任务类用例是否真写死了逐轮"发什么/期望什么",还是泛描述(泛=不合格)?
按统一 schema 输出 + 必补缺口清单(按严重度)。只讲漏洞,不要套话。角色 3 · 可执行性 + 证据诚实性审计
你是可执行性+证据诚实性审计员。默认"AI 拿过去会跑台、有假绿/同义反复",去揪,不附和。
主审:<测试用例-MD 路径>(及其 review HTML 路径)
1. 逐条用例:步骤是否细到 AI 不发明就能执行?点名歧义词("隔离参数""核对状态"等到底是什么)。命令含全部 env 字面值?JSON 可解析?prompt 有一字不差原文?默认值标 文件:行?四项缺一即不合格,列出。
2. 通过标准是否客观二值、可判?
3. 反同义反复:是否存在"没造出会触发的场景却算过""正向断言集为空/零子项静默判过""capture-only 当真Key证明""未实现伪装 PASS/SKIPPED"?
4. 防作弊(独立复跑等)机制是否真能挡住 echo 式作弊?
5. HTML 与 MD 是否真 1:1(抽查若干 TC/§0:字段/步骤/代码块/整节有没有被删或压缩)?
按统一 schema 输出 + 定稿前必须写死/补的清单。给反例,不要套话。---
同样适用于校核本 skill 自身
优化或新增 skill 后,用同 3 角色校核 skill(主审换成 SKILL.md + 各 assets/references),角度对应改为:① 自包含性(规范逻辑是否全有落点、有没有外链依赖)② 完整性一致性(规则间有无矛盾、有无遗漏教训)③ 可操作性(每步有无完成判据、新人只读 skill 能否产出合格物)。
示例:填写参考
以下示例用真实内容演示每个模块应达到的深度,便于你在生成PRD时对齐预期格式和颗粒度。
### 阶段一:任务提交
#### **US-01: 作为POC测试用户,我希望上传多张作业图片并配置批改参数,以便一次性发起批量批改任务。**
* **价值陈述 (Value Statement)**:
* **作为** POC测试用户
* **我希望** 在一个页面完成图片上传与标准答案录入
* **以便于** 用最少的操作触发批量批改并减少配置错误
* **业务规则与逻辑 (Business Logic)**:
1. **前置条件**: 用户已登录;系统已加载可用的批改模型列表。
2. **操作流程 (Happy Path)**:
1. 用户从导航栏进入“批量任务”页面。
2. 左侧“拖拽上传区”选择≤20张、单张≤10MB的图片;文件列表实时展示,支持单项删除。
3. 右侧“多行输入框”按“序号. 单词”格式录入标准答案,系统即时校验序号连续性和英文字符合法性。
4. 用户选择批改模型(默认项)、是否启用“智能终审”、以及批改权重模板(从下拉框加载)。
5. 所有校验通过后,“开始批改”按钮激活;点击后创建任务,页面跳转到任务监控视图。
3. **异常处理 (Error Handling)**:
* 图片体积 >10MB 或扩展名不在白名单时,系统拒绝并提示“请压缩后重试”。
* 标准答案校验失败时,在输入框下方显示具体错误(如“第3行缺少序号”),并禁用提交按钮。
* 创建任务失败且提示为“超出批量上限”时,弹窗提醒“批次上限为20张”,保留已填数据供用户调整。
4. **性能与容量提示**: 单次提交最多20张图片,总体积≤150MB;点击“开始批改”后应在合理时间内给出明确反馈;失败后支持重试并尽量减少用户重复操作。
* **验收标准 (Acceptance Criteria)**:
* **场景1: 成功提交**
* **GIVEN** 我上传了10张图片并输入合法标准答案
* **WHEN** 我点击“开始批改”
* **THEN** 系统应跳转到监控页,显示任务状态为“处理中 0/10”。
* **场景2: 超出批量上限**
* **GIVEN** 我尝试上传第21张图片
* **WHEN** 系统完成校验
* **THEN** 上传被拒绝,提示“单次批改最多20张”,已上传列表保持不变。
---
* **页面布局线框图 (ASCII Wireframe)**:+-------------------------------------------------------------+ | 批量任务创建 | +==============================+==============================+ | 图片上传 (Drag & Drop) | 标准答案 (多行输入) | | - hw_001.jpg [删除] | 1. apple | | - hw_002.jpg [删除] | 2. banana | | ... | ... | +------------------------------+------------------------------+ | 批改模型: [ 默认 v ] 智能终审: [✓] 权重模板: [ 默认 v ] | | [ 取消 ] [ 开始批改 ] | +-------------------------------------------------------------+
HTML 模板填充规范 + MD↔HTML 1:1 校验算法
HTML 只是 MD 的查阅视图:重排版 + 可点击导航 + 大白话,不得引入新事实、删字段、删步骤、压缩整节、合并字段丢文本。
历史上 HTML 反复偷偷压缩 MD(§0 压成一行、卡头字段折进 badge 丢文本)——所以靠下面的机器校验闸,不靠自觉;FAIL 不得交付。
一、填充方式(成本最低)
- 模板:
assets/prd-review.html.tmpl、assets/test-cases-review.html.tmpl,已内置左侧目录、卡片、样式、标记。 - 生成 = 从 MD 抽内容 → 灌进模板占位符,不手写 HTML 结构、不重新设计样式。
- 占位符(以两个 .tmpl 文件实际声明为准,无 `{{BODY}}` 这种通用占位符):
- 两模板通用:
{{PRD_ID}}{{TITLE}}{{STATUS}}(来自 MD 头部)、{{TOC}}(左侧目录,每二级标题/模块/TC 一条<a href="#锚点">) prd-review.html.tmpl:{{OVERVIEW}}(综述§1.1){{JOURNEY}}(旅程地图§1.2){{MERMAID}}(§1.3 的 1.3.1/1.3.2/1.3.3 三个 Mermaid 各一个<pre>原文全进此占位符){{US_CARDS}}(全部 US 卡)test-cases-review.html.tmpl:{{SEC0}}(§0 全文){{PHASE_TABLE}}(§0.5 阶段表){{TC_CARDS}}(全部 TC 卡,按模块前插<h2 id>){{ELSEWHERE}}(末尾「别家怎么减」整表,不可省)- 每个
<h2>/模块/TC 必须带稳定id(如m-a、tc-013)供目录跳转
二、MD→HTML 字段映射(测试用例,13 字段不可丢)
| MD 字段 | HTML DOM |
|---|---|
| 用例编号 | 卡头 .tcid(可见文本,非仅 badge) |
| 用例名称 | 卡头 .tct(可见文本) |
| 所属模块/阶段 | 卡头可见文本或首行 fld(不可只折进 badge 丢文本) |
| 优先级 | badge + 同时保留文本到字段行 |
| 证据类型 | badge(真Key绿/抓包黄/不费Key灰/BLOCKED灰)+ 文本 |
| 前置条件 | fld:逐条 <li>,不合并 |
| 测试数据 | fld:字面值原样;prompt/JSON 用 <pre> 原文,不截断 |
| 测试步骤 | fld:有序列表,每步「动作」+「→ 预期」两段都在 |
| 通过标准 / 失败判定与归类 / 后置清理 / 证据产物 | 各一 fld 行 |
| 代码依据 | fld 或 .dep 行,文件:行 原样 |
| 末尾「别家怎么减」整表 | {{ELSEWHERE}} 内一个 <table>,每行每格文本不丢(历史翻车点,必校) |
PRD 同理:综述(§1.1)/旅程地图(§1.2)/§1.3 三个 Mermaid 代码块/每张 US 卡的全部子项都必须有对应 DOM。Mermaid 必须能渲染:每个图用 <figure class="diagram"> 包一对 <pre>:
<pre class="mermaid">=渲染副本,做 mermaid 必需的安全转义(flowchart 节点文字一律包["…"];文字内 ASCII 双引号"写成#quot;),只为能渲染,不改语义。<details><pre>=源码副本,与 PRD-MD 一字不差的原文,不做任何转义。1:1 校验只认这份(扫<details>里的<pre>)。
渲染失败也有源码可读;渲染出的 SVG 节点文本来自被保留的源码,不算新增事实。禁止深色底:代码块/Mermaid 容器一律浅底深字,pre.mermaid 不得继承深色 pre 背景。
三、MD↔HTML 1:1 校验算法(交付前必跑,FAIL 不交付)
必须用脚本执行,不允许"人肉逐项核对"(人肉=自欺,历史翻车点)。比对前先做文本归一化(两侧同等处理):
- 折叠连续空白/换行为单空格、去首尾空白;
- 全角→半角(标点、数字、ASCII);
- HTML 实体反转义(
<→<、>→>、&→&等); - 去掉 HTML 侧纯装饰文本(目录"目录"标题、badge 纯样式、锚点符号),但字段值/步骤/代码块原文不在去除之列。
1. 解析 MD(归一化后):
- titles = 所有 ## / ### 标题文本集合
- tc_ids = 所有 TC 编号集合
- per TC = {13 字段名 → 字段文本}(步骤拆成每步「动作/预期」两串)
- blocks = 所有代码块(prompt/JSON/Mermaid/逐轮表)原文集合
- sec0 = §0、§0.5 全文逐行
- elsewhere = 末尾「别家怎么减」表每个单元格文本
2. 解析 HTML(归一化后):抽所有锚点 id、可见文本(去标签)、<pre> 原文、表格单元格
3. diff(子串/集合包含判定,均在归一化文本上):
- titles ⊄ HTML 可见文本 → FAIL(标题缺失/被合并)
- tc_ids ⊄ HTML(每个 TC 有独立卡+id) → FAIL
- 任一 TC 任一字段文本 ⊄ 该卡可见文本 → FAIL(字段被删/折叠丢文本)
- 任一 blocks 原文 ⊄ HTML <pre> → FAIL(prompt/JSON/Mermaid/逐轮表被截断)
- sec0 任一行 ⊄ HTML 可见文本 → FAIL(§0/§0.5 被压成一行)
- 任一 elsewhere 单元格 ⊄ HTML 表格文本 → FAIL(别家减项整表被静默丢弃)
- HTML 出现 MD 没有的事实句 → FAIL(引入新事实)
4. 任一 FAIL:列缺失项 → 补回模板 → 重跑脚本,直至 0 FAIL 才交付判定原则:MD 有的,HTML 必须能逐项找到对应可见文本/原文;HTML 多出 MD 没有的事实也 FAIL(不得引入新事实)。允许的差异仅限:排版、加目录锚点、把术语旁注成人话(属呈现,不属增删事实)。
四、交付前自检清单
- [ ] 两份 HTML 均已套对应
.tmpl,未手写结构 - [ ] 左侧目录每条可点击跳转,锚点有效
- [ ] 跑过第三节算法,0 FAIL
- [ ] 卡头未把"编号/名称/模块/优先级/证据类型"折成纯 badge 丢文本
- [ ] prompt/JSON 代码块原文完整,无截断
- [ ] Mermaid 三图均用
<figure class="diagram">包<pre class="mermaid">+<details><pre>,浏览器能渲染出图,源码原文两份齐全无截断 - [ ] §0/§0.5 整节在 HTML 完整可见,未压成一行
- [ ] 窄屏目录可折叠为顶部横排,不遮内容
Mermaid 图示例(需求侧)
用途:用图把"流程/状态/关键交互"讲清楚,减少歧义。
约束:尽量保持在需求层(用户可见行为与系统表现),不要写 API 路径、字段、HTTP code、框架/库。
复杂度控制:单张图建议不超过 15-20 个节点。对于复杂流程,优先"分阶段绘制多张图"而不是一张巨大的图。
示例 1:用户操作流(Flowchart)
场景:手机端「一次性提醒」从列表到创建、触发、处理的闭环。
flowchart TD
A[进入提醒列表] --> B{列表是否为空?}
B -->|是| C[展示空态 + 引导“新建提醒”】【按钮:新建】]
B -->|否| D[展示提醒列表(含状态:未到点/已到点待处理/已完成)]
C --> E[点击“新建提醒”】【进入创建页/弹窗】]
D --> E
E --> F[填写:标题 + 时间]
F --> G[点击“保存”】【返回列表】]
G --> H[列表出现新提醒(未到点)]
H --> I{到达提醒时间?}
I -->|是| J[触发提醒:系统通知 + 应用内弹层(如在前台)]
J --> K{用户选择}
K -->|完成| L[标记已完成 + 从待处理移除]
K -->|延期| M[选择延期时长(例如 10/30 分钟)并重新安排提醒]
K -->|关闭| N[关闭弹层;提醒保留为“已到点待处理”】【在列表可见】]示例 2:状态机(State Diagram)
场景:单个提醒的状态流转(“用户看得见/验收得了”的状态)。
stateDiagram-v2
[*] --> 未到点
未到点 --> 已到点待处理: 到达提醒时间
已到点待处理 --> 已完成: 用户点击“完成”
已到点待处理 --> 未到点: 用户点击“延期”并选择时长
未到点 --> 已取消: 用户删除/取消提醒
已到点待处理 --> 已取消: 用户删除/取消提醒
已取消 --> [*]
已完成 --> [*]示例 3:关键场景时序(Sequence)
场景:创建提醒时的“权限提示/降级路径”(强调用户可见结果,不落到实现)。
sequenceDiagram
participant U as 用户
participant A as App/前台
participant OS as 系统(通知权限/通知中心)
U->>A: 点击“新建提醒”
A-->>U: 展示创建表单(标题、时间)
U->>A: 填写并点击“保存”
A->>OS: 请求/检查通知权限(抽象)
alt 权限已开启
OS-->>A: 允许
A-->>U: 保存成功提示;列表出现新提醒(未到点)
else 权限未开启/被拒绝
OS-->>A: 拒绝
A-->>U: 提示“通知未开启,将仅在列表显示到点待处理”(降级说明)
A-->>U: 仍保存提醒;列表出现新提醒(未到点)
end---
示例 4:B 端后台审批流(Flowchart)
场景:企业内部「报销审批」从提交到审批完成的完整流程(包含驳回重来)。
flowchart TD
A[员工进入报销页面] --> B[填写报销信息:金额 + 类型 + 附件]
B --> C[点击"提交审批"]
C --> D[系统校验]
D --> E{校验通过?}
E -->|否| F[展示错误提示(如"附件缺失")]
F --> B
E -->|是| G[提交成功;状态变为"待审批"]
G --> H[通知审批人(系统通知/邮件)]
H --> I{一级审批人操作}
I -->|通过| J{金额是否 > 5000?}
I -->|驳回| K[状态变为"已驳回";通知员工并显示驳回理由]
K --> L[员工修改后重新提交]
L --> D
J -->|否| M[状态变为"审批通过";通知员工]
J -->|是| N[流转到二级审批人;状态变为"二级审批中"]
N --> O{二级审批人操作}
O -->|通过| M
O -->|驳回| K
M --> P[财务打款;状态变为"已完成"]
P --> Q[结束]示例 5:B 端报销单状态机(State Diagram)
场景:单个报销单在系统中的状态流转(用户和审批人可见的状态)。
stateDiagram-v2
[*] --> 草稿
草稿 --> 待审批: 员工提交
待审批 --> 一级审批中: 分配到审批人
一级审批中 --> 二级审批中: 一级通过 & 金额>5000
一级审批中 --> 审批通过: 一级通过 & 金额≤5000
一级审批中 --> 已驳回: 一级驳回
二级审批中 --> 审批通过: 二级通过
二级审批中 --> 已驳回: 二级驳回
已驳回 --> 草稿: 员工修改
已驳回 --> 已取消: 员工撤销
审批通过 --> 已完成: 财务打款
已取消 --> [*]
已完成 --> [*]示例 6:B 端并发审批时序(Sequence)
场景:当审批需要"多人会签"(所有人都通过才算通过)时的并发处理逻辑。
sequenceDiagram
participant E as 员工
participant S as 系统
participant A1 as 审批人A
participant A2 as 审批人B
participant A3 as 审批人C
E->>S: 提交报销单(金额 8000 元)
S-->>E: 提交成功;状态"待审批"
S->>A1: 通知审批(并发)
S->>A2: 通知审批(并发)
S->>A3: 通知审批(并发)
A1->>S: 通过(1/3)
S-->>E: 更新进度"审批中 1/3"
A3->>S: 通过(2/3)
S-->>E: 更新进度"审批中 2/3"
A2->>S: 驳回
S-->>E: 状态变为"已驳回";显示驳回理由
S-->>A1: 通知"审批已终止"
S-->>A3: 通知"审批已终止"
Note over E,S: 任意一人驳回则整体驳回,<br/>员工修改后重新提交PRD 总集(示例)
用法约定:
- 每新增一个 PRD,就新增一行,给它一个固定的“版本号”(这里作为总集里的唯一标识,不再变)。
- 单个 PRD 文档内部如果需要迭代,用
v0.1 / v0.2 / v1.0自己维护;总集这里不记录内部小版本。
| 版本 | 标题 | 需求内容(详细摘要) | PRD 链接 |
|---|---|---|---|
| PRD-001 | 批量任务创建 | 目标:让 POC 用户在一个页面完成“上传图片 + 录入标准答案 + 配置批改参数 + 发起任务”。范围:支持≤20张、单张≤10MB;支持删除已选图片;标准答案按“序号. 单词”格式输入并实时校验;校验不通过时禁用“开始批改”并提示原因。异常:超限/格式不合法时给出明确提示文案且不破坏已输入内容;创建失败时保留输入并提示可重试。成功:创建后跳转任务监控视图并显示“处理中 x/y”。非目标:不做历史任务一键复用配置。 | docs/prd/PRD-001.md |
| PRD-002 | 任务监控页(列表+详情) | 目标:用户可以查看任务进度与结果,快速定位失败原因。范围:列表展示任务状态(处理中/成功/失败)与进度(x/y),支持按状态筛选;详情展示每张图片的结果与失败原因(用户可读文案)。空/错/加载:空态提供引导;加载失败提供重试且不丢筛选条件;处理中提供持续刷新或手动刷新入口。 | docs/prd/PRD-002.md |
| PRD-003 | 登录态异常统一处理 | 目标:统一“会话过期/登录失效/无权限”的用户提示与跳转规则,减少困惑和误操作。范围:全站统一提示方式;明确不同场景的文案、按钮与跳转目的地;避免用户操作丢失(在可行时提醒保存/稍后重试)。边界:重复触发时不连续弹窗刷屏;从深链进入也能回到正确页面。 | docs/prd/PRD-003.md |
合格测试用例样板(ground-truth · 照这个判"合格长什么样")
给 AI 的锚:用例必须细到"任何 AI 拿过去照跑都不跑台"。下面两条是真实项目(DeepSeek 网关)写到底的样板——普通原子用例 1 条 + 任务类长链路用例 1 条。模板见 assets/test-cases-template.md。---
样板一:普通原子用例(每步动作配预期、数据字面值、带代码依据)
TC-003 网关健康接口在真实 Key 下返回 provider=deepseek、model=deepseek-v4-pro
- 用例编号:TC-003
- 用例名称:网关健康接口在真实 Key 下返回正确 provider 与 model(只验这一个原子点)
- 所属模块/阶段:模块 B 真 Key 基础连通 / 阶段1 连通
- 优先级:P0 阻断
- 证据类型:真Key
- 前置条件:
1. Node ≥16,仓库可跑 npm 2. 有效 DeepSeek Key 已 export DEEPSEEK_API_KEY=<真key> 3. 本机 22000-22999 端口段无占用(provider-smoke.js:21 随机取该段) 4. 无其它进程在写 ~/.deepseek-claude
- 测试数据(精确字面值):
- 完整命令:
DEEPSEEK_API_KEY=<真key> PROVIDER_SMOKE_PROVIDER=deepseek npm run smoke:provider(cwd:deepseek-claude-setup/) - 隐含默认:model=
deepseek-v4-pro(provider-smoke.js:94 → provider.models[0],deepseek.js:41);thinking=enabled(provider-smoke.js:101-102);effort=high(provider-smoke.js:108 写死) - 测试步骤(每步:动作 → 该步预期):
1. 动作:执行上述命令 → 预期:stdout 出现 -- live provider smoke: DeepSeek (deepseek-v4-pro) --(provider-smoke.js:114) 2. 动作:脚本建临时配置目录、部署 proxy bundle、随机端口起网关 → 预期:网关 ≤60s 起来,proxyManager.getHealth() 返回 health 对象(provider-smoke.js:103-117) 3. 动作:脚本执行 assertOk('gateway health', health.provider==='deepseek' && health.model==='deepseek-v4-pro')(provider-smoke.js:118)→ 预期:health.provider 严格 === "deepseek" 且 health.model 严格 === "deepseek-v4-pro" 4. 动作:观察该断言 stdout → 预期:打印一行 OK gateway health(provider-smoke.js:78),未抛错退出
- 通过标准:步骤3布尔为 true 且步骤4打印
OK gateway health,进程未因此 exit 1 - 失败判定与归类:打印
gateway health failed: <JSON>→ provider/model 不符 → gateway(附 redact health JSON);exit 2 → 缺 Key(前置2未满足);网关 60s 未起 → gateway(启动超时) - 后置/清理:脚本 finally
proxyManager.stop()+fs.rmSync(tmp)(provider-smoke.js:137-140) - 证据产物:smoke 全量 stdout(含 OK 行)+ 退出码
- 代码依据:provider-smoke.js:94,101-118;deepseek.js:41
---
样板二:任务类长链路用例(明确任务名 + 逐轮发什么 + 逐轮期望,禁泛化)
注意对比:泛描述「测一个长任务看能不能用」是不合格的;下面这种"任务名 + 每轮发什么内容原文 + 每轮期望"才是合格。
TC-013 codex-long 复杂长链路(Codex 跑 TASK-LONG-TODO)
- 用例编号:TC-013
- 用例名称:真实 Codex 经网关打真实 DeepSeek,在 20 轮内完成 TASK-LONG-TODO 并自测自修到 ALL TESTS PASS
- 所属模块/阶段:模块 D 真 Key Codex 闭环 / 阶段3 复杂长链路
- 优先级:P0 | 证据类型:真Key
- 前置条件:
1. 阶段1、阶段2 全过 2. 有效 DEEPSEEK_API_KEY,可执行 codex CLI(preflight 通过)
- 测试数据(精确字面值):
- 任务名称:TASK-LONG-TODO —— 从零实现"待办 CLI 工具 + 自带测试 + 自跑自修复"
- 任务类型:真实多步骤编程 agent 任务(看懂需求→写多文件→自运行测试→读报错→自修复→直到通过)
- 覆盖周期:约 3~20 个工具轮、约 5~15 分钟
- 轮数规则(写死):一轮 = 网关日志一条
RESPONSES_DONE(runner grep 计数);最少 3 轮、最多 20 轮封顶;超 20 轮未收敛 = 失败,归类 client(模型未在时限收敛),与接入失败分开(不评模型质量) - 每一轮发什么 / 期望什么(逐轮写死):
| 轮次 | 发给模型的内容 | 期望结果(可观测) |
|---|---|---|
| 第 1 轮 | 首轮 verbatim prompt 全文(见下) | 模型发起写文件工具调用,创建 todo.js |
| 第 2 轮 | (agent 自驱,无新增人输入;上下文延续) | 模型创建 test.js,内含 ≥3 处 assert 且有一处同时读 todo.js+todo.json |
| 第 3 轮 | (agent 自驱)执行 node test.js | 模型实际跑测试,stdout 可见测试结果 |
| 第 4~N 轮(若测试未过) | (agent 自驱)按报错改代码后重跑 | 每轮改 1 处后重跑,逐步收敛 |
| 最后一轮 | (agent 自驱)最终运行 | stdout 含精确行 ALL TESTS PASS |
- 首轮 verbatim prompt(一字不差,照抄发送):
在当前目录用 Node.js 完成一个命令行待办工具,下面要求全部做完:
1. 写 todo.js,可执行 CLI,支持三个子命令:
- node todo.js add "<内容>":追加一条待办到当前目录 todo.json
(todo.json 是 JSON 数组,每条形如 {"id":1,"text":"写文档","done":false})
- node todo.js list:按 id 升序打印全部待办,每行格式:
未完成 -> [ ] 1 写文档
已完成 -> [x] 2 修bug
- node todo.js done <id>:把该 id 的待办 done 改为 true
2. 写 test.js,不依赖任何第三方库,只用 Node 内置 assert,覆盖:
- add 两条后 list 输出恰好两行且按 id 升序
- done 第 1 条后,该条在 list 里变成 [x]
- 必须有一个用例同时读取 todo.js 和 todo.json 两个文件内容后再做断言
- 全部断言通过后,最后一行精确打印一行:ALL TESTS PASS
3. 运行 node test.js;若失败,修改代码重试,直到看到 ALL TESTS PASS 为止。- 测试步骤(每步:动作 → 该步预期):
1. 动作:执行 DEEPSEEK_API_KEY=<真key> CLIENT_E2E_PROVIDER=deepseek CLIENT_E2E_TARGETS=codex-tool CLIENT_E2E_LONG=1 npm run e2e:clients → 预期:runner 追加 codex-long case,临时 workspace 起隔离 Codex,把首轮 prompt 原文发给模型 2. 动作:等模型多轮执行 → 预期:网关日志出现 ≥3 次 RESPONSES_DONE 且 ≤20 次 3. 动作:等结束或 900000ms 超时 → 预期:15 分钟内结束,未超时 4. 动作:runner 读临时 workspace → 预期:存在 todo.js/test.js/todo.json,模型自跑 node test.js 输出含精确行 ALL TESTS PASS 5. 动作:runner 独立复跑 node test.js(模型猜不到会复跑) → 预期:复跑退出码 0 且 stdout 含 ALL TESTS PASS 6. 动作:grep 本次网关日志切片 → 预期:不含本次 RESPONSES_FAILED
- 通过标准:步骤2轮数∈[3,20] 且 步骤3未超时 且 步骤4产物齐+模型
ALL TESTS PASS且 步骤5独立复跑也ALL TESTS PASS且 步骤6无RESPONSES_FAILED - 失败判定与归类:超20轮/超时/独立复跑不过 → client(模型未收敛或谎报);三文件缺但有 ALL TESTS PASS 文本 → client(echo 作弊,被步骤5抓出);有
RESPONSES_FAILED→ gateway/provider(发出点 proxy/clients/codex-responses.js:229);claude-long等 target runner 未实现 → BLOCKED-待实现,不得标 PASS/SKIPPED - 后置/清理:随 run 末尾删除临时根目录
- 证据产物:报告 + redact 日志切片(含 RESPONSES_DONE 计数)+ 临时 workspace 三文件快照(不含 key)
- 代码依据:client-e2e.js:242,256;轮信号 RESPONSES_DONE 见 proxy/clients/codex-responses.js:229;任务定义见本用例测试数据
---
自检口诀(产出每条用例前过一遍)
1. 名称只一个原子点?出现"和/且/+"?→ 拆。 2. 每步都有"动作 → 该步预期"?预期是可观测的具体值/日志行? 3. 命令含每个 env 字面值?JSON 可解析?prompt 一字不差有原文?默认值标 文件:行? 4. 任务类:有任务名 + 逐轮"发什么/期望什么"表 + verbatim prompt 全文?没有逐轮表 = 不合格。 5. 每条断言能 grep 到 代码依据 文件:行?grep 不到的字段=自创=删。 6. 证据类型标了?真Key/抓包分清?capture-only 没冒充"通过"? 7. 失败归类到五类之一?未实现标 BLOCKED 没用 PASS/SKIPPED 掩盖?
ASCII 线框图:能力标准与高级示例
以下两个高级示例是绘制标准的质量参考。
示例1:看板风格的项目管理仪表盘
+-----------------------------------------------------------------------------------------+
| 项目仪表盘 [用户头像] [v] |
+-----------------------------------------------------------------------------------------+
| [ 按关键词搜索... ] [ 按用户筛选 v ] [+ 新建任务] |
+-----------------------------------------------------------------------------------------+
| |
| +-----------------------------+ +-----------------------------+ +-----------------------------+
| | 待处理 (3) | | 进行中 (2) | | 已完成 (5) |
| +-----------------------------+ +-----------------------------+ +-----------------------------+
| | | | | | |
| | +-------------------------+ | | +-------------------------+ | | +-------------------------+ |
| | | US-101: 登录与验证 | | | | US-203: 导航与入口 | | | | US-301: 账号与权限设置 | |
| | | #功能 [高] | | | | #杂务 [中] | | | | #基建 [低] | |
| | | 负责人: 爱丽丝 | | | | 负责人: 鲍勃 | | | | 负责人: 卡萝 | |
| | +-------------------------+ | | +-------------------------+ | | +-------------------------+ |
| | | | | | |
| | +-------------------------+ | | | | |
| | | ISSUE-102: 修复崩溃 | | | | | |
| | | #缺陷 [紧急] | | | | | |
| | | 负责人: 鲍勃 | | | | | |
| | +-------------------------+ | | | | |
| | ... | | ... | | ... |
| +-----------------------------+ +-----------------------------+ +-----------------------------+
| |
+-----------------------------------------------------------------------------------------+示例2:带筛选和分页的数据表格页面
+------------------------------------------------------------------------------------+
| 用户管理 [导出CSV] |
+------------------------------------------------------------------------------------+
| 筛选条件: |
| 状态: [ 激活 v ] 角色: [ 所有角色 v ] 添加日期: [YYYY-MM-DD] 至 [YYYY-MM-DD] [应用] |
+------------------------------------------------------------------------------------+
| |
| +----+------------------+-----------------------------+----------+----------------+ |
| | ID | 姓名 | 邮箱 | 角色 | 最后登录 | |
| +----+------------------+-----------------------------+----------+----------------+ |
| | 12 | 爱丽丝·约翰逊 | alice.j@example.com | 管理员 | 2025-09-20 | |
| | 15 | 鲍勃·威廉姆斯 | bob.w@example.com | 编辑 | 2025-09-19 | |
| | 21 | 卡萝·戴维斯 | carol.d@example.com | 查看者 | 2025-09-20 | |
| | .. | ... | ... | ... | ... | |
| +----+------------------+-----------------------------+----------+----------------+ |
| |
| 每页行数: [ 20 v ] << 上一页 | 第 [ 2 ] 页 / 共 15 页 | 下一页 >> |
| |
+------------------------------------------------------------------------------------+<!DOCTYPE html>
<!--
PRD HTML 查阅模板(套用:从 PRD-MD 抽内容灌占位符,禁手写结构/改样式)
占位符:{{PRD_ID}} {{TITLE}} {{STATUS}} {{TOC}} {{OVERVIEW}} {{JOURNEY}} {{MERMAID}} {{US_CARDS}}
生成后必须跑 references/html-fill-spec.md 的 1:1 校验,FAIL 不交付。
规则:与 PRD-MD 严格 1:1,不删故事子项、不丢 Mermaid 代码块原文、不压缩整节。
-->
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>PRD-SAMPLE · 批量作业批改 - V0.5</title>
<style>
:root{
--bg:#f5f6f8;--card:#fff;--ink:#1d2129;--soft:#646a73;--mute:#8a9099;
--line:#e6e8eb;--line2:#eef0f2;
--accent:#3370ff;--accent-d:#1d4fd8;--accent-soft:#eef3ff;--code:#f3f4f6;
--ok:#1f8a5b;--ok-soft:#eaf6f0;--ok-line:#bfe3cf;
}
*{box-sizing:border-box}html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
.shell{max-width:1200px;margin:0 auto;padding:32px 24px 80px;display:flex;gap:28px;align-items:flex-start}
.toc{position:sticky;top:32px;flex:0 0 244px;max-height:calc(100vh - 64px);overflow:auto;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 10px;font-size:13px}
.toc .t{font-weight:700;color:var(--mute);margin:2px 10px 12px;font-size:11px;letter-spacing:1.5px}
.toc a{display:block;color:#3c4148;text-decoration:none;padding:6px 10px;border-radius:8px;margin:1px 0;border-left:2px solid transparent;transition:background .12s,color .12s}
.toc a:hover{background:#f1f2f4;color:var(--ink)}
.toc a.active{background:var(--accent-soft);color:var(--accent-d);font-weight:600;border-left-color:var(--accent)}
.toc a.lv1{font-weight:600;color:var(--ink);margin-top:6px}
.toc a.lv2{padding-left:24px;color:#5c626a;font-size:12.5px}
.toc .grp{font-size:11px;font-weight:700;color:var(--mute);letter-spacing:.5px;margin:14px 10px 4px}
.wrap{flex:1 1 auto;min-width:0;max-width:880px;background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,.04);padding:38px 46px 60px}
@media(max-width:1080px){.shell{flex-direction:column;padding:16px 14px 60px}.toc{position:sticky;top:0;flex:none;width:100%;max-height:none;display:flex;flex-wrap:wrap;gap:2px;z-index:5}.toc .t,.toc .grp{width:100%}.toc a,.toc a.lv2{border-left:none;padding-left:10px}.wrap{max-width:none;padding:24px 18px 40px}}
h1{font-size:24px;margin:0 0 6px;letter-spacing:-.3px}
.sub{color:var(--soft);font-size:13px;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid var(--line)}
h2{font-size:18px;margin:42px 0 14px;padding:2px 0 2px 14px;border-left:4px solid var(--accent);scroll-margin-top:24px;line-height:1.4}
h3{font-size:13.5px;font-weight:700;color:var(--soft);margin:24px 0 8px;letter-spacing:.2px}
h3:before{content:"▸";color:var(--accent);margin-right:7px}
p{margin:8px 0}
code{background:var(--code);border-radius:5px;padding:1.5px 5px;font:12.5px ui-monospace,Menlo,monospace;word-break:break-word}
pre{background:#f7f8fa;color:#1d2129;border:1px solid var(--line);border-radius:10px;padding:13px 15px;font:12.5px/1.65 ui-monospace,Menlo,monospace;white-space:pre-wrap;margin:8px 0;overflow:auto}
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13.5px}
th,td{border:1px solid var(--line);padding:8px 11px;text-align:left;vertical-align:top}th{background:#f5f6f8;font-weight:600}
ul,ol{margin:6px 0;padding-left:22px}li{margin:5px 0}ol ol,ul ul,ol ul,ul ol{margin:4px 0}
.fld{margin:16px 20px}
.fk{display:inline-block;font-size:12px;font-weight:700;color:var(--accent-d);background:var(--accent-soft);border-radius:6px;padding:2px 9px;margin-bottom:8px;letter-spacing:.3px}
.fv{font-size:14.5px}
.us{background:#fff;border:1px solid var(--line);border-radius:14px;padding:0;margin:22px 0;scroll-margin-top:24px;overflow:hidden}
.ush{display:flex;align-items:baseline;gap:11px;background:linear-gradient(180deg,#f4f7fd,#fbfcfe);border-bottom:1px solid var(--line);padding:15px 20px}
.usid{font:800 13px ui-monospace,Menlo,monospace;color:#fff;background:var(--accent);border-radius:6px;padding:3px 9px;flex:none;align-self:flex-start}
.ustitle{font-size:15px;font-weight:700;line-height:1.5}
.us .fld:first-of-type{margin-top:18px}
.fld.wf pre{background:#fbfcfd}
.fld.acc .fv strong{color:var(--accent-d)}
.diagram{background:#fbfcfd;border:1px solid var(--line);border-radius:12px;padding:18px;margin:14px 0;overflow:auto}
.diagram .mermaid{background:none;border:0;padding:0;margin:0;color:inherit;white-space:normal;display:flex;justify-content:center;text-align:center;min-height:32px}
.diagram .mermaid svg{max-width:100%;height:auto}
.diagram details{margin-top:10px}
.diagram summary{cursor:pointer;color:var(--soft);font-size:12px;user-select:none}
.diagram details pre{margin-top:8px}
.foot{color:var(--mute);font-size:12.5px;margin-top:42px;border-top:1px solid var(--line);padding-top:16px}
</style></head><body>
<div class="shell">
<nav class="toc"><div class="t">目录</div><a class="lv1" href="#overview">1 综述</a>
<a class="lv2" href="#journey">1.2 用户旅程地图</a>
<a class="lv2" href="#mermaid">1.3 流程/状态/时序图</a>
<a class="lv1" href="#stories">2 用户故事</a>
<a class="lv2" href="#us-01">US-01 上传图片并配置参数</a>
<a class="lv2" href="#us-02">US-02 监控任务进度</a></nav>
<main class="wrap">
<h1>PRD-SAMPLE · 批量作业批改 - V0.5</h1>
<div class="sub">状态:Draft | Frozen(待定) · 本页与 PRD-MD 严格 1:1,不压缩 · 配套:PRD-SAMPLE-测试用例</div>
<h2 id="overview">1. 综述</h2>
<h3>1.1 项目背景与核心问题</h3>
<div>教师在 POC 阶段需要批量批改英语单词作业。当前流程为逐张人工核对,单批 20 张需 30 分钟以上,且标准答案录入易出错。本需求提供一个一站式页面:一次性上传多张作业图片、录入标准答案、配置批改模型与权重,触发批量批改并跳转监控视图,把单批操作压缩到分钟级,并把配置错误在提交前拦截掉。</div>
<h2 id="journey">1.2 用户旅程地图</h2>
<div>
<ol>
<li><strong>阶段一:任务提交</strong> - 用户在单页完成图片上传与标准答案录入,配置批改参数并触发批量批改任务。</li>
<li><strong>阶段二:任务监控</strong> - 用户在监控视图实时查看批改进度与逐张结果,对失败项发起重试。</li>
</ol>
</div>
<h2 id="mermaid">1.3 流程/状态/时序图</h2>
<h3>1.3.1 用户操作流(必填)</h3>
<figure class="diagram">
<pre class="mermaid">flowchart TD
A["开始:用户进入批量任务页"] --> B["上传图片 + 录入标准答案"]
B --> C{"校验:数量 / 体积 / 序号连续性"}
C -->|全部通过| D["激活 #quot;开始批改#quot; 并创建任务"]
C -->|失败 / 异常| E["输入框下方提示具体错误,禁用提交"]
D --> F["跳转任务监控视图"]
E --> B
F --> G["结束"]</pre>
<details><summary>查看 Mermaid 源码</summary><pre>flowchart TD
A[开始:用户进入批量任务页] --> B[上传图片 + 录入标准答案]
B --> C{校验:数量/体积/序号连续性}
C -->|全部通过| D[激活"开始批改"并创建任务]
C -->|失败/异常| E[输入框下方提示具体错误,禁用提交]
D --> F[跳转任务监控视图]
E --> B
F --> G[结束]</pre></details>
</figure>
<h3>1.3.2 状态机(当存在明确状态流转对象时必填)</h3>
<figure class="diagram">
<pre class="mermaid">stateDiagram-v2
[*] --> 草稿
草稿 --> 已提交: 点击开始批改
已提交 --> 处理中: 系统开始批改
处理中 --> 成功: 全部批改完成
处理中 --> 失败: 存在批改失败项
失败 --> 已提交: 用户对失败项重试
成功 --> [*]</pre>
<details><summary>查看 Mermaid 源码</summary><pre>stateDiagram-v2
[*] --> 草稿
草稿 --> 已提交: 点击开始批改
已提交 --> 处理中: 系统开始批改
处理中 --> 成功: 全部批改完成
处理中 --> 失败: 存在批改失败项
失败 --> 已提交: 用户对失败项重试
成功 --> [*]</pre></details>
</figure>
<h3>1.3.3 关键场景时序(仅当“时序/并发/重试/超时”影响用户可见结果时填写)</h3>
<figure class="diagram">
<pre class="mermaid">sequenceDiagram
participant U as 用户
participant A as App/前台
participant S as 批改系统
U->>A: 点击"开始批改"
A->>S: 提交批量批改任务(图片集 + 标准答案)
alt 成功
S-->>A: 返回任务ID与初始状态"处理中 0/N"
A-->>U: 跳转监控页,展示"处理中 0/N"
else 失败/异常
S-->>A: 返回失败原因(如超出批量上限)
A-->>U: 弹窗提示并保留已填数据供调整
end</pre>
<details><summary>查看 Mermaid 源码</summary><pre>sequenceDiagram
participant U as 用户
participant A as App/前台
participant S as 批改系统
U->>A: 点击"开始批改"
A->>S: 提交批量批改任务(图片集 + 标准答案)
alt 成功
S-->>A: 返回任务ID与初始状态"处理中 0/N"
A-->>U: 跳转监控页,展示"处理中 0/N"
else 失败/异常
S-->>A: 返回失败原因(如超出批量上限)
A-->>U: 弹窗提示并保留已填数据供调整
end</pre></details>
</figure>
<h2 id="stories">2. 用户故事</h2>
<div class="us" id="us-01">
<div class="ush"><span class="usid">US-01</span><div class="ustitle">作为 POC 测试用户,我希望上传多张作业图片并配置批改参数,以便一次性发起批量批改任务。</div></div>
<div class="fld"><div class="fk">价值陈述</div><div class="fv">
<ul>
<li><strong>作为</strong> POC 测试用户</li>
<li><strong>我希望</strong> 在一个页面完成图片上传与标准答案录入</li>
<li><strong>以便于</strong> 用最少的操作触发批量批改并减少配置错误</li>
</ul></div></div>
<div class="fld"><div class="fk">业务规则与逻辑</div><div class="fv">
<ol>
<li><strong>前置条件</strong>:用户已登录;系统已加载可用的批改模型列表。</li>
<li><strong>操作流程 (Happy Path)</strong>:
<ol>
<li>用户从导航栏进入“批量任务”页面。</li>
<li>左侧“拖拽上传区”选择≤20张、单张≤10MB的图片;文件列表实时展示,支持单项删除。</li>
<li>右侧“多行输入框”按“序号. 单词”格式录入标准答案,系统即时校验序号连续性和英文字符合法性。</li>
<li>用户选择批改模型(默认项)、是否启用“智能终审”、以及批改权重模板(从下拉框加载)。</li>
<li>所有校验通过后,“开始批改”按钮激活;点击后创建任务,页面跳转到任务监控视图。</li>
</ol>
</li>
<li><strong>异常处理 (Error Handling)</strong>:
<ul>
<li>图片体积 >10MB 或扩展名不在白名单时,系统拒绝并提示“请压缩后重试”。</li>
<li>标准答案校验失败时,在输入框下方显示具体错误(如“第3行缺少序号”),并禁用提交按钮。</li>
<li>创建任务失败且提示为“超出批量上限”时,弹窗提醒“批次上限为20张”,保留已填数据供用户调整。</li>
</ul>
</li>
<li><strong>性能与容量提示</strong>:单次提交最多20张图片,总体积≤150MB;点击“开始批改”后应在合理时间内给出明确反馈;失败后支持重试并尽量减少用户重复操作。</li>
</ol></div></div>
<div class="fld acc"><div class="fk">验收标准</div><div class="fv">
<p><strong>场景1: 成功提交</strong></p>
<ul>
<li><strong>GIVEN</strong> 我上传了10张图片并输入合法标准答案</li>
<li><strong>WHEN</strong> 我点击“开始批改”</li>
<li><strong>THEN</strong> 系统应跳转到监控页,显示任务状态为“处理中 0/10”。</li>
</ul>
<p><strong>场景2: 超出批量上限</strong></p>
<ul>
<li><strong>GIVEN</strong> 我尝试上传第21张图片</li>
<li><strong>WHEN</strong> 系统完成校验</li>
<li><strong>THEN</strong> 上传被拒绝,提示“单次批改最多20张”,已上传列表保持不变。</li>
</ul></div></div>
<div class="fld wf"><div class="fk">页面布局线框图</div><pre>+-------------------------------------------------------------+
| 批量任务创建 |
+==============================+==============================+
| 图片上传 (Drag & Drop) | 标准答案 (多行输入) |
| - hw_001.jpg [删除] | 1. apple |
| - hw_002.jpg [删除] | 2. banana |
| ... | ... |
+------------------------------+------------------------------+
| 批改模型: [ 默认 v ] 智能终审: [✓] 权重模板: [ 默认 v ] |
| [ 取消 ] [ 开始批改 ] |
+-------------------------------------------------------------+</pre></div>
</div>
<div class="us" id="us-02">
<div class="ush"><span class="usid">US-02</span><div class="ustitle">作为 POC 测试用户,我希望实时查看批改进度并对失败项重试,以便确认批量结果并补救个别异常。</div></div>
<div class="fld"><div class="fk">价值陈述</div><div class="fv">
<ul>
<li><strong>作为</strong> POC 测试用户</li>
<li><strong>我希望</strong> 在监控视图看到逐张批改进度与结果</li>
<li><strong>以便于</strong> 第一时间发现失败项并仅对失败项发起重试,而不必整批重跑</li>
</ul></div></div>
<div class="fld"><div class="fk">业务规则与逻辑</div><div class="fv">
<ol>
<li><strong>前置条件</strong>:US-01 已成功创建任务并跳转至监控视图。</li>
<li><strong>操作流程 (Happy Path)</strong>:
<ol>
<li>监控视图轮询任务状态,顶部进度条显示“处理中 m/N”。</li>
<li>列表逐张展示结果:成功项标“已批改”,失败项标“失败”并附原因。</li>
<li>全部完成后,状态变为“成功”,提供导出结果入口。</li>
</ol>
</li>
<li><strong>异常处理 (Error Handling)</strong>:
<ul>
<li>个别图片识别失败时,该项状态置“失败”,提供“重试本项”按钮,不影响其它项。</li>
<li>轮询接口连续 3 次超时,顶部提示“连接不稳定,正在重连”,保留已展示进度。</li>
</ul>
</li>
</ol></div></div>
<div class="fld acc"><div class="fk">验收标准</div><div class="fv">
<p><strong>场景1: 全部成功</strong></p>
<ul>
<li><strong>GIVEN</strong> 我提交了10张图片的批改任务</li>
<li><strong>WHEN</strong> 全部10张批改完成</li>
<li><strong>THEN</strong> 顶部状态显示“成功 10/10”,并出现“导出结果”入口。</li>
</ul>
<p><strong>场景2: 单项失败重试</strong></p>
<ul>
<li><strong>GIVEN</strong> 第3张图片批改失败并标“失败”</li>
<li><strong>WHEN</strong> 我点击该项“重试本项”</li>
<li><strong>THEN</strong> 仅第3项重新进入“处理中”,其余9项结果保持不变。</li>
</ul></div></div>
</div>
<div class="foot">本页给人查阅,全部内容来自 PRD-MD,仅翻译排版、未增删事实。Mermaid 图浏览器内渲染,源码原文在「查看 Mermaid 源码」内完整保留。</div>
</main></div>
<script>
(function(){
var links=[].slice.call(document.querySelectorAll('.toc a[href^="#"]'));
var map=links.map(function(a){return{a:a,el:document.getElementById(a.getAttribute('href').slice(1))};}).filter(function(x){return x.el;});
function onScroll(){
var cur=null;
for(var i=0;i<map.length;i++){ if(map[i].el.getBoundingClientRect().top<=140) cur=map[i]; else break; }
if(!cur&&map.length) cur=map[0];
links.forEach(function(a){a.classList.remove('active');});
if(cur) cur.a.classList.add('active');
}
window.addEventListener('scroll',onScroll,{passive:true});
window.addEventListener('resize',onScroll); onScroll();
})();
</script>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({startOnLoad:true,theme:'default',securityLevel:'loose',flowchart:{useMaxWidth:true,htmlLabels:true},sequence:{useMaxWidth:true},themeVariables:{fontFamily:'-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif'}});
</script>
</body></html>
<!DOCTYPE html>
<!--
测试用例 HTML 查阅模板(套用:从 测试用例-MD 抽内容灌占位符,禁手写结构/改样式)
占位符:{{PRD_ID}} {{STATUS}} {{TOC}} {{SEC0}} {{PHASE_TABLE}} {{TC_CARDS}} {{ELSEWHERE}}
生成后必须跑 references/html-fill-spec.md 的 1:1 校验,FAIL 不交付。
规则:与 MD 严格 1:1,不删字段/不删步骤/不压缩整节;13 字段按三组归拢,文字不动。
-->
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>PRD-SAMPLE 测试用例</title>
<style>
:root{
--bg:#f5f6f8;--card:#fff;--ink:#1d2129;--soft:#646a73;--mute:#8a9099;
--line:#e6e8eb;--line2:#eef0f2;
--accent:#3370ff;--accent-d:#1d4fd8;--accent-soft:#eef3ff;--code:#f3f4f6;
--g2:#6b4ef0;--g3:#1f8a5b;
--ok:#1f8a5b;--ok-soft:#eaf6f0;--ok-line:#bfe3cf;
--warn:#b4541f;--warn-soft:#fdf1ea;--warn-line:#f0d2bd;
--p0:#a23b2e;--p1:#8a5e12;--p2:#5b6168;--real:#1f8a5b;--cap:#8a5e12;--blk:#5b6168;
}
*{box-sizing:border-box}html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
.shell{max-width:1200px;margin:0 auto;padding:32px 24px 80px;display:flex;gap:28px;align-items:flex-start}
.toc{position:sticky;top:32px;flex:0 0 244px;max-height:calc(100vh - 64px);overflow:auto;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 10px;font-size:13px}
.toc .t{font-weight:700;color:var(--mute);margin:2px 10px 12px;font-size:11px;letter-spacing:1.5px}
.toc a{display:block;color:#3c4148;text-decoration:none;padding:6px 10px;border-radius:8px;margin:1px 0;border-left:2px solid transparent;transition:background .12s,color .12s}
.toc a:hover{background:#f1f2f4;color:var(--ink)}
.toc a.active{background:var(--accent-soft);color:var(--accent-d);font-weight:600;border-left-color:var(--accent)}
.toc a.lv1{font-weight:600;color:var(--ink);margin-top:6px}
.toc a.lv2{padding-left:24px;color:#5c626a;font-size:12.5px}
.toc .grp{font-size:11px;font-weight:700;color:var(--mute);letter-spacing:.5px;margin:14px 10px 4px}
.wrap{flex:1 1 auto;min-width:0;max-width:900px;background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,.04);padding:38px 46px 60px}
@media(max-width:1080px){.shell{flex-direction:column;padding:16px 14px 60px}.toc{position:sticky;top:0;flex:none;width:100%;max-height:none;display:flex;flex-wrap:wrap;gap:2px;z-index:5}.toc .t,.toc .grp{width:100%}.toc a,.toc a.lv2{border-left:none;padding-left:10px}.wrap{max-width:none;padding:24px 18px 40px}}
h1{font-size:24px;margin:0 0 6px;letter-spacing:-.3px}
.sub{color:var(--soft);font-size:13px;margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--line)}
h2{font-size:18px;margin:42px 0 14px;padding:2px 0 2px 14px;border-left:4px solid var(--accent);scroll-margin-top:24px;line-height:1.4}
h2.mod{border-left-color:var(--g2)}
code{background:var(--code);border-radius:5px;padding:1.5px 5px;font:12px ui-monospace,Menlo,monospace;word-break:break-word}
pre{background:#f7f8fa;color:#1d2129;border:1px solid var(--line);border-radius:10px;padding:12px 14px;font:12px/1.65 ui-monospace,Menlo,monospace;white-space:pre-wrap;margin:6px 0;overflow:auto}
table{width:100%;border-collapse:collapse;margin:10px 0;font-size:13px}
th,td{border:1px solid var(--line);padding:7px 9px;text-align:left;vertical-align:top}th{background:#f5f6f8;font-weight:600}
ul,ol{margin:6px 0;padding-left:22px}li{margin:5px 0}
.legend{margin:14px 0 0;font-size:13px}
.tc{background:#fff;border:1px solid var(--line);border-radius:14px;padding:0;margin:22px 0;scroll-margin-top:24px;overflow:hidden}
.tch{display:flex;gap:12px;background:linear-gradient(180deg,#f4f7fd,#fbfcfe);border-bottom:1px solid var(--line);padding:15px 20px}
.tcid{font:800 13px ui-monospace,Menlo,monospace;color:#fff;background:var(--accent);border-radius:6px;padding:3px 9px;height:fit-content;flex:none}
.tch-main{min-width:0}
.tct{font-size:15.5px;font-weight:700;line-height:1.45;margin-bottom:8px}
.tcmeta{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.tcmod{font-size:12px;color:var(--soft)}
.badge{font-size:11px;font-weight:700;border-radius:5px;padding:2px 8px;border:1px solid}
.b-p0{color:#fff;background:var(--p0);border-color:var(--p0)}
.b-p1{color:var(--p1);background:#fbf3df;border-color:#e6cf9b}
.b-p2{color:var(--p2);background:#eef0f2;border-color:#d3d7dc}
.b-real{color:#fff;background:var(--real);border-color:var(--real)}
.b-cap{color:var(--cap);background:#fbf3df;border-color:#e6cf9b}
.b-blk{color:#fff;background:var(--blk);border-color:var(--blk)}
.grp{border-top:1px solid var(--line2)}
.grp:first-of-type{border-top:0}
.gh{display:flex;align-items:center;gap:9px;font-size:12px;font-weight:700;color:var(--accent-d);letter-spacing:.5px;padding:14px 20px 2px}
.gn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:var(--accent);color:#fff;font:700 11px ui-monospace,Menlo,monospace}
.grp.g-exec .gn{background:var(--g2)}.grp.g-exec .gh{color:var(--g2)}
.grp.g-judge .gn{background:var(--g3)}.grp.g-judge .gh{color:var(--g3)}
.grp .fld{margin:12px 20px 16px}
.idgrid{display:grid;grid-template-columns:max-content 1fr;gap:7px 18px;font-size:13.5px;margin:12px 20px 16px}
.idgrid dt{color:var(--soft);font-weight:600}
.idgrid dd{margin:0}
.fld{margin:14px 20px}
.fk{display:inline-block;font-size:12px;font-weight:700;color:var(--accent-d);background:var(--accent-soft);border-radius:6px;padding:2px 9px;margin-bottom:8px;letter-spacing:.3px}
.fv{font-size:14px}
ol.steps{list-style:none;margin:6px 0;padding:0;counter-reset:st}
ol.steps>li{position:relative;counter-increment:st;padding:10px 0 10px 34px;border-bottom:1px dashed var(--line2)}
ol.steps>li:last-child{border-bottom:0}
ol.steps>li:before{content:counter(st);position:absolute;left:0;top:8px;width:22px;height:22px;border-radius:50%;background:var(--accent-soft);color:var(--accent-d);font:700 12px ui-monospace,Menlo,monospace;display:flex;align-items:center;justify-content:center}
.exp{display:block;margin-top:7px;padding:7px 11px;background:var(--ok-soft);border-left:3px solid var(--ok);border-radius:0 6px 6px 0;color:#1c5c3e;font-size:13px}
.exp:before{content:"预期 → ";font-weight:700;color:var(--ok)}
.pass-box{background:var(--ok-soft);border:1px solid var(--ok-line);border-left:4px solid var(--ok);border-radius:8px;padding:11px 14px;font-size:14px;color:#1c5c3e;font-weight:600}
.fail-box{background:var(--warn-soft);border:1px solid var(--warn-line);border-left:4px solid var(--warn);border-radius:8px;padding:9px 14px}
.fail-box ul{margin:4px 0}
ul.tight{margin:4px 0;padding-left:20px}ul.tight>li{margin:4px 0}
.dep{font-size:12px;color:var(--mute);background:#fafbfc;border-top:1px solid var(--line2);padding:11px 20px;font-family:ui-monospace,Menlo,monospace;word-break:break-word}
.foot{color:var(--mute);font-size:12.5px;margin-top:42px;border-top:1px solid var(--line);padding-top:16px}
</style></head><body>
<div class="shell">
<nav class="toc"><div class="t">目录</div><a class="lv1" href="#sec0">0 全局约定</a>
<a class="lv1" href="#phase">0.5 测试阶段编排</a>
<div class="grp">测试模块</div>
<a class="lv1" href="#m-b">模块 B · 真 Key 基础连通</a>
<a class="lv2" href="#tc-003">TC-003 网关健康接口</a>
<a class="lv1" href="#m-d">模块 D · 真 Key Codex 闭环</a>
<a class="lv2" href="#tc-013">TC-013 codex-long 长链路</a>
<a class="lv2" href="#tc-014">TC-014 claude-long 长链路</a>
<a class="lv1" href="#elsewhere">别家怎么减</a></nav>
<main class="wrap">
<h1>PRD-SAMPLE 测试用例</h1>
<div class="sub">状态:Draft | Frozen(待定) · 本页与 测试用例-MD 严格 1:1,不压缩 · 一条用例=一个原子验证点</div>
<table class="legend"><tr><th>标记</th><th>含义</th></tr>
<tr><td><span class="badge b-p0">P0</span></td><td>阻断级,不过则不通过</td></tr>
<tr><td><span class="badge b-real">真Key</span></td><td>打真实上游,证"真能用"</td></tr>
<tr><td><span class="badge b-cap">抓包</span></td><td>假上游,只证"字段对",不证能用</td></tr>
<tr><td><span class="badge b-blk">BLOCKED</span></td><td>未实现,非通过非跳过</td></tr></table>
<h2 id="sec0">0 全局约定</h2>
<ul>
<li><strong>工作目录 / 命令前缀</strong>:所有命令在 <code>deepseek-claude-setup/</code> 目录下跑(cwd 固定)。</li>
<li><strong><code><真key></code> 等占位</strong>:真实凭据通过环境变量 <code>DEEPSEEK_API_KEY</code> 传入,禁止写进文件或日志。</li>
<li><strong>环境隔离</strong>:每条用例建临时配置目录、部署 proxy bundle、随机端口起网关,不碰真实 ~/.deepseek-claude(代码依据 provider-smoke.js:21,137-140)。</li>
<li><strong>两类证据</strong>:<code>真Key</code>=打真实上游证"真能用";<code>抓包</code>=假上游恒回固定值只证"字段对",不证能用。</li>
<li><strong>门禁句(必写)</strong>:发「PRD-SAMPLE 通过」必须全部真 Key 阻断用例(TC-003、TC-013)全绿;capture-only 不发通过。</li>
<li><strong>失败统一五类</strong>:preflight / gateway / provider / client / cleanup(按失败最早环节归唯一一类;cleanup 一票否决)。</li>
</ul>
<h2 id="phase">0.5 测试阶段编排</h2>
<table>
<tr><th>阶段</th><th>测什么点</th><th>包含用例</th><th>门禁</th></tr>
<tr><td>阶段0 资格地基(不费Key)</td><td>底层 patcher/restore 边界</td><td>TC-001…</td><td>不过 → 全流程停</td></tr>
<tr><td>阶段1 连通(先证明能连)</td><td>gateway 直连 + 客户端最简连通</td><td>TC-003…</td><td>全过 → 进阶段2,否则停</td></tr>
<tr><td>阶段2 能力(简单任务测开关/切换)</td><td>思考/强度/工具/换模型</td><td>TC-008…</td><td>全过 → 进阶段3</td></tr>
<tr><td>阶段3 复杂长链路</td><td>明确长任务 TASK-LONG-TODO(各客户端各一)</td><td>TC-013、TC-014</td><td>全过 → 通过</td></tr>
<tr><td>贯穿·安全</td><td>配置不被改/不泄密/清理</td><td>TC-020…</td><td>任一不过 → 对应用例一票否决</td></tr>
</table>
<h2 class="mod" id="m-b">模块 B · 真 Key 基础连通</h2>
<div class="tc" id="tc-003">
<div class="tch"><span class="tcid">TC-003</span>
<div class="tch-main">
<div class="tct">网关健康接口在真实 Key 下返回正确 provider 与 model(只验这一个原子点)</div>
<div class="tcmeta"><span class="tcmod">模块 B 真 Key 基础连通 / 阶段1 连通</span>
<span class="badge b-p0">P0 阻断</span><span class="badge b-real">真Key</span></div>
</div></div>
<div class="grp">
<div class="gh"><span class="gn">1</span>用例身份与前提</div>
<dl class="idgrid">
<dt>所属模块/阶段</dt><dd>模块 B 真 Key 基础连通 / 阶段1 连通</dd>
<dt>优先级</dt><dd>P0 阻断</dd>
<dt>证据类型</dt><dd>真Key</dd>
</dl>
<div class="fld"><div class="fk">前置条件</div><div class="fv"><ul class="tight">
<li>Node ≥16,仓库可跑 npm</li>
<li>有效 DeepSeek Key 已 <code>export DEEPSEEK_API_KEY=<真key></code></li>
<li>本机 22000-22999 端口段无占用(provider-smoke.js:21 随机取该段)</li>
<li>无其它进程在写 ~/.deepseek-claude</li>
</ul></div></div>
</div>
<div class="grp g-exec">
<div class="gh"><span class="gn">2</span>如何执行</div>
<div class="fld"><div class="fk">测试数据(精确字面值)</div><div class="fv"><ul class="tight">
<li>完整命令:<code>DEEPSEEK_API_KEY=<真key> PROVIDER_SMOKE_PROVIDER=deepseek npm run smoke:provider</code>(cwd: <code>deepseek-claude-setup/</code>)</li>
<li>隐含默认:model=<code>deepseek-v4-pro</code>(provider-smoke.js:94 → provider.models[0],deepseek.js:41);thinking=<code>enabled</code>(provider-smoke.js:101-102);effort=<code>high</code>(provider-smoke.js:108 写死)</li>
</ul></div></div>
<div class="fld"><div class="fk">测试步骤</div><div class="fv"><ol class="steps">
<li>动作:执行上述命令<span class="exp">stdout 出现 <code>-- live provider smoke: DeepSeek (deepseek-v4-pro) --</code>(provider-smoke.js:114)</span></li>
<li>动作:脚本建临时配置目录、部署 proxy bundle、随机端口起网关<span class="exp">网关 ≤60s 起来,<code>proxyManager.getHealth()</code> 返回 health 对象(provider-smoke.js:103-117)</span></li>
<li>动作:脚本执行 <code>assertOk('gateway health', health.provider==='deepseek' && health.model==='deepseek-v4-pro')</code>(provider-smoke.js:118)<span class="exp"><code>health.provider</code> 严格 === <code>"deepseek"</code> 且 <code>health.model</code> 严格 === <code>"deepseek-v4-pro"</code></span></li>
<li>动作:观察该断言 stdout<span class="exp">打印一行 <code> OK gateway health</code>(provider-smoke.js:78),未抛错退出</span></li>
</ol></div></div>
</div>
<div class="grp g-judge">
<div class="gh"><span class="gn">3</span>如何判定</div>
<div class="fld"><div class="fk">通过标准</div><div class="fv"><div class="pass-box">步骤3布尔为 true 且步骤4打印 <code>OK gateway health</code>,进程未因此 exit 1</div></div></div>
<div class="fld"><div class="fk">失败判定与归类</div><div class="fv"><div class="fail-box"><ul class="tight">
<li>打印 <code>gateway health failed: <JSON></code> → provider/model 不符 → gateway(附 redact health JSON)</li>
<li>exit 2 → 缺 Key(前置2未满足)</li>
<li>网关 60s 未起 → gateway(启动超时)</li>
</ul></div></div></div>
<div class="fld"><div class="fk">后置/清理</div><div class="fv">脚本 finally <code>proxyManager.stop()</code> + <code>fs.rmSync(tmp)</code>(provider-smoke.js:137-140)</div></div>
<div class="fld"><div class="fk">证据产物</div><div class="fv">smoke 全量 stdout(含 OK 行)+ 退出码</div></div>
</div>
<div class="dep">代码依据:provider-smoke.js:94,101-118;deepseek.js:41</div>
</div>
<h2 class="mod" id="m-d">模块 D · 真 Key Codex 闭环(复杂长链路)</h2>
<div class="tc" id="tc-013">
<div class="tch"><span class="tcid">TC-013</span>
<div class="tch-main">
<div class="tct">真实 Codex 经网关打真实 DeepSeek,在 20 轮内完成 TASK-LONG-TODO 并自测自修到 ALL TESTS PASS</div>
<div class="tcmeta"><span class="tcmod">模块 D 真 Key Codex 闭环 / 阶段3 复杂长链路</span>
<span class="badge b-p0">P0</span><span class="badge b-real">真Key</span></div>
</div></div>
<div class="grp">
<div class="gh"><span class="gn">1</span>用例身份与前提</div>
<dl class="idgrid">
<dt>所属模块/阶段</dt><dd>模块 D 真 Key Codex 闭环 / 阶段3 复杂长链路</dd>
<dt>优先级</dt><dd>P0</dd>
<dt>证据类型</dt><dd>真Key</dd>
</dl>
<div class="fld"><div class="fk">前置条件</div><div class="fv"><ul class="tight">
<li>阶段1、阶段2 全过</li>
<li>有效 <code>DEEPSEEK_API_KEY</code>,可执行 <code>codex</code> CLI(preflight 通过)</li>
</ul></div></div>
</div>
<div class="grp g-exec">
<div class="gh"><span class="gn">2</span>如何执行</div>
<div class="fld"><div class="fk">测试数据(精确字面值)</div><div class="fv">
<ul class="tight">
<li><strong>任务名称</strong>:TASK-LONG-TODO —— 从零实现"待办 CLI 工具 + 自带测试 + 自跑自修复"</li>
<li><strong>任务类型</strong>:真实多步骤编程 agent 任务(看懂需求→写多文件→自运行测试→读报错→自修复→直到通过)</li>
<li><strong>覆盖周期</strong>:约 3~20 个工具轮、约 5~15 分钟</li>
<li><strong>轮数规则(写死)</strong>:一轮 = 网关日志一条 <code>RESPONSES_DONE</code>(runner grep 计数);最少 3 轮、最多 20 轮封顶;超 20 轮未收敛 = 失败,归类 <strong>client(模型未在时限收敛)</strong>,与接入失败分开(不评模型质量)</li>
</ul>
<p><strong>每一轮发什么 / 期望什么(逐轮写死)</strong>:</p>
<table>
<tr><th>轮次</th><th>发给模型的内容</th><th>期望结果(可观测)</th></tr>
<tr><td>第 1 轮</td><td>首轮 verbatim prompt 全文(见下)</td><td>模型发起写文件工具调用,创建 <code>todo.js</code></td></tr>
<tr><td>第 2 轮</td><td>(agent 自驱,无新增人输入;上下文延续)</td><td>模型创建 <code>test.js</code>,内含 ≥3 处 <code>assert</code> 且有一处同时读 <code>todo.js</code>+<code>todo.json</code></td></tr>
<tr><td>第 3 轮</td><td>(agent 自驱)执行 <code>node test.js</code></td><td>模型实际跑测试,stdout 可见测试结果</td></tr>
<tr><td>第 4~N 轮(若测试未过)</td><td>(agent 自驱)按报错改代码后重跑</td><td>每轮改 1 处后重跑,逐步收敛</td></tr>
<tr><td>最后一轮</td><td>(agent 自驱)最终运行</td><td>stdout 含精确行 <code>ALL TESTS PASS</code></td></tr>
</table>
<p><strong>首轮 verbatim prompt(一字不差,照抄发送)</strong>:</p>
<pre>在当前目录用 Node.js 完成一个命令行待办工具,下面要求全部做完:
1. 写 todo.js,可执行 CLI,支持三个子命令:
- node todo.js add "<内容>":追加一条待办到当前目录 todo.json
(todo.json 是 JSON 数组,每条形如 {"id":1,"text":"写文档","done":false})
- node todo.js list:按 id 升序打印全部待办,每行格式:
未完成 -> [ ] 1 写文档
已完成 -> [x] 2 修bug
- node todo.js done <id>:把该 id 的待办 done 改为 true
2. 写 test.js,不依赖任何第三方库,只用 Node 内置 assert,覆盖:
- add 两条后 list 输出恰好两行且按 id 升序
- done 第 1 条后,该条在 list 里变成 [x]
- 必须有一个用例同时读取 todo.js 和 todo.json 两个文件内容后再做断言
- 全部断言通过后,最后一行精确打印一行:ALL TESTS PASS
3. 运行 node test.js;若失败,修改代码重试,直到看到 ALL TESTS PASS 为止。</pre>
</div></div>
<div class="fld"><div class="fk">测试步骤</div><div class="fv"><ol class="steps">
<li>动作:执行 <code>DEEPSEEK_API_KEY=<真key> CLIENT_E2E_PROVIDER=deepseek CLIENT_E2E_TARGETS=codex-tool CLIENT_E2E_LONG=1 npm run e2e:clients</code><span class="exp">runner 追加 codex-long case,临时 workspace 起隔离 Codex,把首轮 prompt 原文发给模型</span></li>
<li>动作:等模型多轮执行<span class="exp">网关日志出现 ≥3 次 <code>RESPONSES_DONE</code> 且 ≤20 次</span></li>
<li>动作:等结束或 900000ms 超时<span class="exp">15 分钟内结束,未超时</span></li>
<li>动作:runner 读临时 workspace<span class="exp">存在 <code>todo.js</code>/<code>test.js</code>/<code>todo.json</code>,模型自跑 <code>node test.js</code> 输出含精确行 <code>ALL TESTS PASS</code></span></li>
<li>动作:runner <strong>独立复跑</strong> <code>node test.js</code>(模型猜不到会复跑)<span class="exp">复跑退出码 0 且 stdout 含 <code>ALL TESTS PASS</code></span></li>
<li>动作:grep 本次网关日志切片<span class="exp">不含本次 <code>RESPONSES_FAILED</code></span></li>
</ol></div></div>
</div>
<div class="grp g-judge">
<div class="gh"><span class="gn">3</span>如何判定</div>
<div class="fld"><div class="fk">通过标准</div><div class="fv"><div class="pass-box">步骤2轮数∈[3,20] 且 步骤3未超时 且 步骤4产物齐+模型 <code>ALL TESTS PASS</code> 且 步骤5独立复跑也 <code>ALL TESTS PASS</code> 且 步骤6无 <code>RESPONSES_FAILED</code></div></div></div>
<div class="fld"><div class="fk">失败判定与归类</div><div class="fv"><div class="fail-box"><ul class="tight">
<li>超20轮/超时/独立复跑不过 → client(模型未收敛或谎报)</li>
<li>三文件缺但有 ALL TESTS PASS 文本 → client(echo 作弊,被步骤5抓出)</li>
<li>有 <code>RESPONSES_FAILED</code> → gateway/provider(发出点 proxy/clients/codex-responses.js:229)</li>
<li><code>claude-long</code> 等 target runner 未实现 → <strong>BLOCKED-待实现,不得标 PASS/SKIPPED</strong></li>
</ul></div></div></div>
<div class="fld"><div class="fk">后置/清理</div><div class="fv">随 run 末尾删除临时根目录</div></div>
<div class="fld"><div class="fk">证据产物</div><div class="fv">报告 + redact 日志切片(含 RESPONSES_DONE 计数)+ 临时 workspace 三文件快照(不含 key)</div></div>
</div>
<div class="dep">代码依据:client-e2e.js:242,256;轮信号 RESPONSES_DONE 见 proxy/clients/codex-responses.js:229;任务定义见本用例测试数据</div>
</div>
<div class="tc" id="tc-014">
<div class="tch"><span class="tcid">TC-014</span>
<div class="tch-main">
<div class="tct">真实 Claude Code 经网关打真实 DeepSeek,在 20 轮内完成 TASK-LONG-TODO 并自测自修到 ALL TESTS PASS</div>
<div class="tcmeta"><span class="tcmod">模块 D 真 Key Codex 闭环 / 阶段3 复杂长链路</span>
<span class="badge b-p0">P0</span><span class="badge b-blk">BLOCKED</span></div>
</div></div>
<div class="grp">
<div class="gh"><span class="gn">1</span>用例身份与前提</div>
<dl class="idgrid">
<dt>所属模块/阶段</dt><dd>模块 D 真 Key Codex 闭环 / 阶段3 复杂长链路</dd>
<dt>优先级</dt><dd>P0</dd>
<dt>证据类型</dt><dd>BLOCKED(target <code>claude-long</code> runner 未实现,非通过非跳过)</dd>
</dl>
<div class="fld"><div class="fk">前置条件</div><div class="fv"><ul class="tight">
<li>阶段1、阶段2 全过</li>
<li>有效 <code>DEEPSEEK_API_KEY</code>,可执行 <code>claude</code> CLI(preflight 通过)</li>
</ul></div></div>
</div>
<div class="grp g-exec">
<div class="gh"><span class="gn">2</span>如何执行</div>
<div class="fld"><div class="fk">测试数据(精确字面值)</div><div class="fv"><ul class="tight">
<li><strong>任务名称</strong>:TASK-LONG-TODO —— 与 TC-013 同题,换 Claude Code 客户端跑以便横向对比</li>
<li><strong>当前状态</strong>:<code>client-e2e.js</code> 暂未实现 <code>claude-long</code> target 的多轮 runner,无法执行该用例</li>
<li>首轮 verbatim prompt:同 TC-013 原文(一字不差,照抄发送)</li>
</ul></div></div>
<div class="fld"><div class="fk">测试步骤</div><div class="fv"><ol class="steps">
<li>动作:执行 <code>DEEPSEEK_API_KEY=<真key> CLIENT_E2E_PROVIDER=deepseek CLIENT_E2E_TARGETS=claude-tool CLIENT_E2E_LONG=1 npm run e2e:clients</code><span class="exp">runner 报 <code>claude-long</code> target 未实现并标 BLOCKED,不进入多轮</span></li>
<li>动作:检查报告该 case 状态<span class="exp">状态记为 <code>BLOCKED-待实现</code>,未伪装 PASS 或 SKIPPED</span></li>
</ol></div></div>
</div>
<div class="grp g-judge">
<div class="gh"><span class="gn">3</span>如何判定</div>
<div class="fld"><div class="fk">通过标准</div><div class="fv"><div class="pass-box">该 target runner 实现并补齐后,转用 TC-013 同口径标准评估;当前状态合法值仅为 BLOCKED</div></div></div>
<div class="fld"><div class="fk">失败判定与归类</div><div class="fv"><div class="fail-box"><ul class="tight">
<li><code>claude-long</code> target runner 未实现 → <strong>BLOCKED-待实现(不得标 PASS/SKIPPED)</strong></li>
<li>若被标成 PASS 或 SKIPPED 掩盖未实现 → 视为认证作弊,门禁拦截</li>
</ul></div></div></div>
<div class="fld"><div class="fk">后置/清理</div><div class="fv">无产物生成;无临时目录需清理</div></div>
<div class="fld"><div class="fk">证据产物</div><div class="fv">报告中该 case 的 BLOCKED 状态行 + 未实现原因</div></div>
</div>
<div class="dep">代码依据:client-e2e.js:242,256(target 分发未覆盖 claude-long 分支)</div>
</div>
<h2 id="elsewhere">别家怎么减</h2>
<p>本家(DeepSeek)最全样板;别家同 N 条结构按真实 capabilities 人工减/换。</p>
<table>
<tr><th>别家差异(依据 provider 定义 文件:行)</th><th>受影响用例及如何减/换</th></tr>
<tr><td>智谱 BigModel <code>capabilities.thinking=false</code>(zai.js:38)</td><td>TC-003 删去 thinking=enabled 断言;改为断言请求体 <strong>mustNotHave</strong> thinking 字段</td></tr>
<tr><td>Kimi <code>capabilities.effort=false</code>(kimi.js:35)</td><td>TC-003 删 effort=high 默认值校验;其余 provider/model 断言保留</td></tr>
<tr><td>某 provider 未提供 Codex 客户端路径(runtime-config.js path=null)</td><td>TC-013 对应 codex-long 标 SKIPPED;若 runner 未实现则标 BLOCKED,不得 PASS</td></tr>
</table>
<p>标准化实质:结构、13 字段、命令骨架、判定口径对所有家一致;别家在本家这套上按其真实 capabilities <strong>人工</strong>减/换字段断言(参照断言库规则,非自动框架)。</p>
<div class="foot">本页给人查阅,全部内容来自 测试用例-MD(每条带代码依据 文件:行),仅翻译排版、未增删事实。一条用例=一个原子验证点,13 字段,每步动作配预期。</div>
</main></div>
<script>
(function(){
var links=[].slice.call(document.querySelectorAll('.toc a[href^="#"]'));
var map=links.map(function(a){return{a:a,el:document.getElementById(a.getAttribute('href').slice(1))};}).filter(function(x){return x.el;});
function onScroll(){
var cur=null;
for(var i=0;i<map.length;i++){ if(map[i].el.getBoundingClientRect().top<=140) cur=map[i]; else break; }
if(!cur&&map.length) cur=map[0];
links.forEach(function(a){a.classList.remove('active');});
if(cur) cur.a.classList.add('active');
}
window.addEventListener('scroll',onScroll,{passive:true});
window.addEventListener('resize',onScroll); onScroll();
})();
</script>
</body></html>