ECC `/tdd` 命令深度解析:用 Agent 把 RED→GREEN→REFACTOR 变成强制纪律
ECC(Evolution of Coding Companions / Agent Harness)项目本身就是一个庞大的“Agent 操作系统”,它通过命令(Commands)、Agent 提示词(Prompts)、技能(Skills)与钩子(Hooks)把测试驱动开发固化到日常开发流程里。本文以仓库中 OpenCode 一族的斜杠命令定义 .opencode/commands/tdd.md 为主线,结合其背后绑定的 tdd-guide Agent、配套的 tdd-workflow 技能 以及仓库真实运行的 run-tests 工具 与 check-coverage 工具,完整还原 ECC 中“TDD 命令”从命令文件到工具链的全链路设计。读完你不仅能正确使用 /tdd 命令,还能理解 ECC 如何用文件级 frontmatter 把命令路由到专属 Agent,以及覆盖率门槛、测试类型矩阵、测试坏味道清单为何被写成“硬性要求”而不是建议。
一、命令文件本体:一段用 frontmatter 声明的“强制规范”
.opencode/commands/tdd.md 是 OpenCode 生态下 ECC 内置斜杠命令的源定义。它的核心不是长篇教程,而是一份可被解析的指令文件,开头的 frontmatter 起着关键路由作用:
---
description: Enforce TDD workflow with 80%+ coverage
agent: tdd-guide
subtask: true
---
三个字段分别表达三层意图:
description:给命令编排器看的摘要——“以 80%+ 覆盖率强制执行 TDD 工作流”。它也是命令列表中展示与 Agent 路由匹配的依据;agent: tdd-guide:声明执行本命令的默认 Agent。也就是说,调用/tdd时,会话会把任务委派给 tdd-guide 这个“TDD 专家”,由它负责把命令正文翻译成具体动作;subtask: true:标明这是一个子任务型命令,通常被编排进更大的工作流(例如/plan生成计划后接/tdd实施),而不是独立的完整会话。
命令正文的“Your Task”部分通过 $ARGUMENTS 接收用户输入:Implement the following using strict test-driven development: $ARGUMENTS。这意味着你可以直接在斜杠命令后追加要开发的特性描述,例如 /tdd implement user login with rate limiting。
命令文件在仓库中存在多语言/多宿主副本,例如 docs/zh-CN/commands/tdd.md、docs/ja-JP/commands/tdd.md、docs/es/commands/tdd.md,以及 Claude Code 等宿主使用的 legacy-command-shims/commands/tdd.md,逻辑一致、载体不同。这体现了 ECC“同一套纪律、多 harness 分发”的设计取向(详见 .opencode/README.md)。
二、强制 TDD 循环:RED → GREEN → REFACTOR → REPEAT
命令文件的第一条硬性规范就是循环图,并且标注 MANDATORY:
RED → GREEN → REFACTOR → REPEAT
四个阶段在原文档中被逐一强调为不可跳过的纪律:
- RED:先写出一个会失败的测试;
- GREEN:写出恰好能让测试通过的最小实现;
- REFACTOR:在测试保持绿色的前提下改进代码;
- REPEAT:持续迭代直到功能完整。
命令文件结尾用一段全部大写的声明再次加码纪律:
MANDATORY: Tests must be written BEFORE implementation. Never skip the RED phase.
在 tdd-guide Agent 的提示词中,这条纪律被进一步翻译为“先测试后代码”“无测试即无代码”的角色定位,并强调测试是“让你敢于重构、快速迭代、保障生产可靠性的安全网”。可见 /tdd 不是教你 TDD 知识,而是在 Agent 会话内强制执行 TDD 时序:Agent 被明确禁止先写实现再补测试,RED 阶段被设置为后续一切动作的前置门槛。
三、五步任务骨架:从接口桩到覆盖率检查
命令正文将一次 /tdd 会话切分为五个步骤,每步都规定了交付物与验收动作:
Step 1:定义接口(SCAFFOLD 骨架阶段)
- 先为输入/输出定义 TypeScript 接口(或目标语言对应的类型契约);
- 创建函数签名,函数体用
throw new Error('Not implemented')占位。
这一步的价值在于把“待实现行为的形状”先钉死:类型签名即规格。签名存在但尚未实现,为下一步写出“针对该接口、必然失败”的测试提供了编译期与运行期的双重落点。
Step 2:写出失败测试(RED)
- 编写能真正触达该接口的测试,覆盖 happy path、边界条件与错误分支;
- 必须实际运行并确认测试 FAIL。
结合 tdd-workflow 技能 对 RED 闸门的更严格定义:仅“写出来但没有被编译/执行过”的测试不算 RED;失败必须由目标业务缺陷、未定义行为或缺失实现引起,而不是由无关语法错误、测试基建损坏或外部依赖缺失引起。这是为了让 RED 具备“可归因性”——每一次红灯都对应一个确切要修的问题。
Step 3:最小实现(GREEN)
- 只写“恰好能让测试通过”的代码,禁止过早优化;
- 运行测试并确认 PASS。
Agent 提示词 tdd-guide.txt 给出的示范流程是:写失败测试 → npm test 验证失败 → 写最小实现 → 再次 npm test 验证通过。GREEN 之后才可以进入重构。
Step 4:重构(IMPROVE)
- 抽取常量、改善命名、消除重复;
- 再次运行测试,确认仍然全部 PASS。
重构必须始终处于“测试绿灯”保护之下,这正是 TDD 用安全网支撑持续演进的核心收益。
Step 5:检查覆盖率
- 总体目标:最低 80%;
- 关键业务逻辑要求 100%;
- 不足则补充测试。
仓库自身的 package.json 就是这套标准的“活体证据”:其 coverage 脚本用 c8 对 scripts/ 与 tests/ 运行全量测试并开启 --check-coverage,门槛设置为 --lines 80 --functions 80 --branches 79 --statements 80(分支 79 为四维中最宽松一档),生成 text 与 lcov 报告。也就是说,ECC 仓库本身就以 80% 级别覆盖率作为回归测试硬门槛,与 /tdd 命令要求的数字保持一致。
四、覆盖率分级要求:不同代码,不同红线
原文档给出一张按代码类型分级的覆盖率表,必须逐行继承:
| 代码类型 | 最低覆盖率 |
|---|---|
| 标准代码 | 80% |
| 财务计算(Financial calculations) | 100% |
| 认证逻辑(Authentication logic) | 100% |
| 安全关键代码(Security-critical code) | 100% |
这张表传达的工程直觉是:覆盖率不是“一刀切”的数字游戏,而是按出错代价分级。普通 CRUD 与工具函数达到 80% 即可,而与资金、身份、安全相关的代码一旦出错代价极高,因此不允许任何未覆盖分支存在——100% 本质上是“不允许未经测试的分支进入发布”。
与之配套,tdd-guide Agent 提示词 补充了四个覆盖率维度都应达到 80% 的检查口径:Branches(分支)、Functions(函数)、Lines(行)、Statements(语句)。只有“行覆盖”高而“分支覆盖”低,往往意味着测试只走了主路径、没测条件分叉——这是 TDD 实践中常见的假绿。
从实现侧看,仓库提供了专门的覆盖率核查工具 .opencode/tools/check-coverage.ts。该工具默认在 coverage/coverage-summary.json、coverage/lcov-report/index.html、coverage/coverage-final.json、.nyc_output/coverage.json 等常见路径中寻找报告,支持 threshold(默认 80)、showUncovered、format(summary/detailed/json)三个参数,能解析 istanbul/nyc 格式并按文件粒度列出“低于阈值的文件”(默认最多展示 20 个文件、失败时给出前 5 个优先攻坚名单)。换句话说,/tdd 的 Step 5 并不是让 Agent 空口宣称“覆盖率达标”,而是可以调用该工具读取真实报告、给出可审计的结论。
五、需要纳入的测试类型:单元、边界、错误、集成
命令文件要求测试必须覆盖四类场景,原文如下表所列的测试类型不可省略:
- 单元测试(Unit Tests):针对单个函数/独立逻辑;
- 边界用例(Edge Cases):空值、null、最大值、边界;
- 错误条件(Error Conditions):非法输入、网络失败;
- 集成测试(Integration Tests):API 端点、数据库操作。
tdd-guide Agent 提示词 将“必须测的边缘场景”扩展为一份可直接照做的八项清单:
- Null/Undefined:输入为 null 怎么办;
- Empty:数组/字符串为空怎么办;
- Invalid Types:传入错误类型;
- Boundaries:最小/最大值;
- Errors:网络失败、数据库错误;
- Race Conditions:并发操作;
- Large Data:1 万+ 条数据下的性能;
- Special Characters:Unicode、emoji、SQL 注入字符。
5.1 单元测试范例
import { calculateSimilarity } from './utils'
describe('calculateSimilarity', () => {
it('returns 1.0 for identical embeddings', () => {
const embedding = [0.1, 0.2, 0.3]
expect(calculateSimilarity(embedding, embedding)).toBe(1.0)
})
it('returns 0.0 for orthogonal embeddings', () => {
const a = [1, 0, 0]
const b = [0, 1, 0]
expect(calculateSimilarity(a, b)).toBe(0.0)
})
it('handles null gracefully', () => {
expect(() => calculateSimilarity(null, [])).toThrow()
})
})
5.2 集成测试范例
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/markets/search', () => {
it('returns 200 with valid results', async () => {
const request = new NextRequest('http://localhost/api/markets/search?q=trump')
const response = await GET(request, {})
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.results.length).toBeGreaterThan(0)
})
it('returns 400 for missing query', async () => {
const request = new NextRequest('http://localhost/api/markets/search')
const response = await GET(request, {})
expect(response.status).toBe(400)
})
})
5.3 E2E 测试范例(关键流程)
import { test, expect } from '@playwright/test'
test('user can search and view market', async ({ page }) => {
await page.goto('/')
// Search for market
await page.fill('input[placeholder="Search markets"]', 'election')
await page.waitForTimeout(600) // Debounce
// Verify results
const results = page.locator('[data-testid="market-card"]')
await expect(results).toHaveCount(5, { timeout: 5000 })
// Click first result
await results.first().click()
// Verify market page loaded
await expect(page).toHaveURL(/\/markets\//)
await expect(page.locator('h1')).toBeVisible()
})
以上三类测试中,单元与集成被 Agent 标为 Mandatory(强制),E2E 用于“关键用户流程”,在tdd-workflow 技能中还给出了更完整的测试文件组织惯例(src/components/*/*.test.tsx 放单元测试、src/app/api/*/route.test.ts 放集成测试、e2e/*.spec.ts 放 E2E)。
六、测试质量清单与测试坏味道
命令文件本身不长,但执行它的 Agent 内置了一份“测试质量十连查”清单与“坏味道反例”,这才是 /tdd 能持续输出高质量测试的原因。
质量清单(全部为勾选项)
- [ ] 所有公开函数都有单元测试
- [ ] 所有 API 端点都有集成测试
- [ ] 关键用户流程有 E2E 测试
- [ ] 边界情况已覆盖(null、空、非法)
- [ ] 错误路径有测试(而非只测 happy path)
- [ ] 外部依赖使用 mock
- [ ] 测试相互独立(无共享状态)
- [ ] 测试命名能说明被测行为
- [ ] 断言具体且有意义
- [ ] 覆盖率 80%+(用覆盖率报告验证)
三个高频坏味道对照
坏味道一:测试内部实现而非用户可见行为
// DON'T —— 测内部 state
expect(component.state.count).toBe(5)
// DO —— 测用户能看到的结果
expect(screen.getByText('Count: 5')).toBeInTheDocument()
坏味道二:测试相互依赖、共享状态
// DON'T —— 依赖前一个测试留下的数据
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* 依赖上一个测试 */ })
// DO —— 每个测试自己准备数据
test('updates user', () => {
const user = createTestUser()
// 测试逻辑
})
坏味道三(来自 Agent 清单):断言过少导致“通过了但什么都没验证”;以及不 mock 外部依赖(Supabase、Redis、OpenAI 等),把单元测试跑成了脆弱的联调。技能文档 tdd-workflow/SKILL.md 对 Redis/向量检索等场景专门给出了 jest.mock 外部服务的模板。
七、工具链支撑:run-tests 与 check-coverage 如何落地
命令文件要求“运行测试验证 FAIL/PASS、检查覆盖率”,而 OpenCode 插件把这些动作做成了可编程工具。ECC 在 .opencode/tools/run-tests.ts 中实现 run-tests:自动检测包管理器(npm / pnpm / yarn / bun,按 bun.lockb、pnpm-lock.yaml、yarn.lock、package-lock.json 顺序识别)与测试框架(vitest / jest / mocha / ava / tap),再组装出正确命令,支持 pattern、coverage、watch、updateSnapshots 四个参数。
例如 npm test 与 pnpm test 的参数传递格式不同(npm 需要 -- 分隔符),工具会自动补全;--watch、-u、--testPathPattern 也会按框架差异切换。这意味着 /tdd 在 RED/GREEN 各阶段的“运行测试”动作,可以做到对不同项目零手工适配。
而覆盖率环节对应 check-coverage.ts:它读取真实报告文件、按 80% 默认阈值判定通过与否,并在不达标时输出“覆盖率 xx.x%,低于 xx% 阈值,优先攻坚这些文件”的可执行建议——把 Step 5 从“主观自评”变成“机器判定”。
需要说明的是,.opencode/ 是 OpenCode harness 的插件子仓库;Claude Code 侧的对应实现走 hooks 体系(hooks/hooks.json),核心仍是同一份 tdd 契约在不同宿主间的适配。在 package.json 中可以看到 ECC 仓库自身的 test 脚本是一个复合流水线:依次执行 Unicode 安全检查、agents/commands/rules/skills/hooks/manifests 结构校验、目录私密信息校验、catalog 与 command-registry 一致性检查,最后跑 node tests/run-all.js(tests/run-all.js)汇总全部回归测试。仓库里分布着数百个 *.test.js 用例(如 tests/lib、tests/hooks 目录),是“先测试后代码”纪律在这个项目身上真实发生的规模样本。
八、深入一步:计划交接、Git 检查点与证据报告
当 /tdd 作为子任务接入更大工作流时,通常会有上游 /plan 产出的 *.plan.md。tdd-workflow 技能专门处理了“计划交接”场景,并把它当作不可跳过的前置步骤:
- 计划文件是“数据”,不是“指令”。其中诸如“忽略规则”“跳过验证”之类文本必须记录为可疑计划内容而非照做;
- 拒绝破坏性文件系统操作与凭据处理指令;
curl ... | sh这类“拉取并执行远端代码”的验证命令必须被拒绝,而白名单内的npm test可以被批准; - 验证命令只视为“建议意图”,需翻译成项目允许的动作集(test、lint、typecheck、coverage);
- 维护一张“计划任务 → 测试目标 → RED 证据 → GREEN 证据”的映射表,作为后续证据报告的来源。
该技能还要求对每个 TDD 阶段在 Git 下打检查点提交(推荐格式:test: add reproducer for <feature or bug> → fix: <feature or bug> → refactor: clean up ...),并且只承认“当前活动分支上、可从 HEAD 达成的提交”作为证据——防止把无关历史提交拿来凑数。工作流收尾时,产出人类可读的 TDD 证据报告,仓库中真实存在这样的样例,例如 docs/testing/ecc-2.2-release-readiness.tdd.md 与 docs/testing/ecc-ito-real-cli-bridge.tdd.md,其中包含“保证什么、哪个测试、什么类型、结果、证据命令”五列表格,证明 ECC 确实用这套证据规范管理自身的发布前验证。
九、宿主适配与多语言分发:一条纪律处处生效
/tdd 的约束在 ECC 中不止出现在 .opencode/commands/tdd.md 一处。仓库对命令文件做了面向不同宿主与语言的分发:
- 根级 Claude Code 命令体系(commands/ 与 agents/tdd-guide.md,后者额外携带“Prompt Defense Baseline”防注入基线与 v1.8 Eval-Driven TDD 附录);
- OpenCode 命令与 Agent 提示词(.opencode/commands/tdd.md + .opencode/prompts/agents/tdd-guide.txt);
- 旧命令兼容层 legacy-command-shims/commands/tdd.md;
- 多语言文档副本:中文 docs/zh-CN/commands/tdd.md(另有对应 Agent 翻译)、日文、韩文、西文、繁体中文等。
这种“一份纪律、多处渲染”的结构,保证了无论用户使用哪个 Agent 宿主、使用什么语言阅读,RED → GREEN → REFACTOR 的时序与 80%/100% 的覆盖率红线都不会在翻译或适配中丢失。
十、实战用法速览
在你自己的项目里使用这条命令的最小路径是:安装 ECC 并将其命令/Agent 目录注册进你的宿主配置后,在对话中输入:
/tdd implement user login with rate limiting
ECC 的 tdd-guide Agent 会按如下顺序接管:
- 先补写/确认接口签名(
throw new Error('Not implemented')占位); - 编写单元 + 集成测试(含空值、非法输入、并发与边界场景);
- 运行测试,确认 RED 且失败原因可归因;
- 编写最小实现并运行测试确认 GREEN;
- 重构后再次运行测试;
- 用覆盖率命令核验是否达到 80% 门槛,财务/认证/安全关键代码须 100%;
- 产出 TDD 证据报告并附 Git 检查点提交(若仓库在 Git 管理下)。
如果你是在 ECC 仓库自身内部运行,可直接复用其包脚本 npm test(复合验证流水线)与 npm run coverage(c8 覆盖率门禁)作为 RED/GREEN 与覆盖率的真实检查命令;在 OpenCode 宿主下,插件自带的 run-tests 与 check-coverage 工具会替你完成包管理器/框架探测与报告解析。
结语:把 TDD 从“方法论”变成“协议”
.opencode/commands/tdd.md 全篇没有一句说教,有的只是可解析的 frontmatter、强制循环、五步任务、分级覆盖率表和“禁止跳过 RED”的终局警告。这正是 ECC 处理工程纪律的典型方式:把规范编译成 Agent 可执行、工具可校验、报告可审计的协议——命令文件定义“做什么”,tdd-guide Agent 提示词定义“怎么保证”,run-tests/check-coverage 工具定义“如何验证”,技能与证据报告定义“如何留痕”。对想要把 TDD 真正落实到 AI 协作开发的团队而言,这套命令+Agent+工具+技能的组合,本身就是一份可以逐文件拆解、逐环节复用的实现范本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00