关于 tdd
tdd 是一种通过多个 Agent 协作执行测试驱动开发的技能,适用于希望先编写测试再实现功能的开发流程。它用独立的测试编写、实现和重构 Agent 隔离规格与实现上下文,并支持交互、自动、恢复和试运行模式。
主要功能
- 01按 RED-GREEN-REFACTOR 循环编排
- 02测试编写与实现 Agent 分离
- 03按架构层次拆分垂直切片
- 04支持交互、自动与恢复模式
- 05试运行时渲染提示词并验证流程
适用场景
- 01为新功能先建立测试覆盖
- 02先编写复现测试再修复缺陷
- 03检查 TDD 脚本和提示词而不写代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ glebis/ claude-skills --skill tdd安装后在 Claude Code 中输入/tdd确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonreferences/agent_prompts.mdanti_patterns.mdframework_configs.mdlayer_guide.mdscripts/discover_docs.shextract_api.shrun_tests.shtests/conftest.pytest_discover_docs.pytest_extract_api.pytest_prompts.pytest_run_tests.pytests/fixtures/go_project/calculator.gotests/fixtures/python_project/README.mdtests/fixtures/python_project/docs/API.mdopenapi.yamltests/fixtures/python_project/src/calculator.pymodels.pytests/fixtures/ts_project/README.mdtests/fixtures/ts_project/src/calculator.tstests/snapshots/implementer.txtrefactorer.txttest_writer.txt详细介绍
测试驱动开发 — 多 Agent 编排
使用独立的子 Agent分别编写测试和实现,严格执行 RED-GREEN-REFACTOR 循环。核心创新:**测试编写者永远看不到实现代码,实现者永远看不到规格说明。**这可以防止 LLM 将实现意图泄露到测试设计中。
适用场景
- 用户要求采用 TDD、测试优先或 red-green-refactor 工作流
- 用户提供功能描述或 Bug 报告,并输入
/tdd - 用户希望添加功能,并从一开始就确保有测试覆盖
- 用户希望先编写可复现 Bug 的测试,再修复 Bug
调用模式
| 调用方式 | 行为 |
|---|---|
/tdd <feature> | 交互模式 — 在各个切片和每个 RED 检查点暂停并等待批准 |
/tdd --auto <feature> | 自主模式 — 不暂停,运行所有切片;仅在遇到不可恢复错误时停止 |
/tdd --resume | 从项目根目录的 .tdd-state.json 恢复 |
/tdd --dry-run <feature> | 验证模式 — 完整运行 Phase 0 + Phase 1,渲染所有提示词,但跳过 Task() 调用。不写入代码。 |
在 --auto 模式下,跳过所有 [HUMAN CHECKPOINT] 步骤。改为打印状态行:
[auto] RED slice 1/4: "validates email format" — test failing as expected
[auto] GREEN slice 1/4: passing (attempt 1)
[auto] REFACTOR slice 1/4: 1 suggestion applied, 0 skipped
仅在以下情况下停止并询问用户:
- 经过 5 次尝试后,实现仍然失败
- 经过 3 次尝试后,仍无法自动修复回归问题
- 脚本错误导致无法继续(缺少二进制文件、权限被拒绝等)
在 --dry-run 模式下,验证完整的编排流程,但不执行任何子 Agent,也不写入任何代码:
- 完整运行 Phase 0:检测框架、验证基线、提取 API、发现文档、创建状态文件
- 完整运行 Phase 1:分解为多个切片(仍需用户批准)
- 针对每个切片:使用实际变量渲染全部三份 Agent 提示词(测试编写者、实现者、重构者)。向用户打印渲染后的提示词及其字符数。
- 不调用
Task()。不写入测试文件。不生成实现代码。 - 验证:检查所有模板变量是否已解析(不存在
{UNRESOLVED}占位符)、所有脚本能否无错误执行,以及状态文件格式是否正确。 - 报告摘要:
DRY RUN COMPLETE: {feature name}
Phase 0:
Framework: {framework}
Language: {language}
Baseline: {pass|greenfield}
API surface: {line count} lines
Doc context: {line count} lines (or "none")
Phase 1:
Slices: {N} ({layer breakdown})
Prompts rendered: {N * 3} (all variables resolved)
Test Writer: {char count} chars
Implementer: {char count} chars
Refactorer: {char count} chars
State file: .tdd-state.json written
No code was modified.
此模式适用于:
- 验证脚本能否在项目环境中正常运行
- 在决定完整运行 TDD 流程前,检查提示词内容
- 在不产生副作用的情况下测试技能变更
架构概览
ORCHESTRATOR (you, reading this file)
├─ Phase 0: Setup — detect framework, extract API, create state file
├─ Phase 1: Decompose into vertical slices → user approves
│
├─ FOR EACH SLICE:
│ ├─ Phase 2 (RED): Task(Test Writer) ← spec + API only
│ ├─ Phase 3 (GREEN): Task(Implementer) ← failing test + error only
│ └─ Phase 4 (REFACTOR): Task(Refactorer) ← all code + green results
│
└─ Summary
上下文边界(关键约束)
| Agent | 可见内容 | 不可见内容 |
|---|---|---|
| 测试编写者 | 切片规格、公共 API 签名、框架约定、分层约束 | 实现代码、其他切片、实现计划 |
| 实现者 | 失败的测试代码、测试失败输出、文件树、现有源代码、分层约束 | 原始规格、切片描述、未来计划 |
| 重构者 | 所有实现代码 + 所有测试 + 通过测试的结果、涉及的分层 | 原始规格、分解依据 |
工作流
阶段 0:设置(每个会话执行一次)
步骤 1:检测框架和测试运行器。
Check for: package.json (jest/vitest), pyproject.toml/pytest.ini (pytest),
go.mod (go test), Cargo.toml (cargo test), Gemfile (rspec), composer.json (phpunit)
如果无法确定,请询问:“运行测试的命令是什么?(例如,npm test、pytest)”
步骤 2:从源文件检测语言(用于 Agent 提示词):
TypeScript (.ts/.tsx), JavaScript (.js/.jsx), Python (.py), Go (.go), Rust (.rs), Ruby (.rb), PHP (.php)
步骤 3:验证基线是否通过。
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{TEST_COMMAND}"
解析 JSON 输出。
- 如果
status为"pass":继续。 - 如果
status为"fail":停止——“现有测试未通过。TDD 应从通过测试的基线开始。” - 如果
status为"error"且total为 0:绿地项目——目前还没有测试。这没有问题。继续。
步骤 4:提取公共 API 接口。
bash ~/.claude/skills/tdd/scripts/extract_api.sh {SOURCE_DIR}
保存输出——测试编写者会看到这些内容。如果输出为空(绿地项目),这是预期情况。
步骤 5:发现项目文档。
bash ~/.claude/skills/tdd/scripts/discover_docs.sh {PROJECT_ROOT} --lang {LANGUAGE}
此操作会搜索:
- 文档文件:README、ARCHITECTURE.md、docs/ 文件夹、DESIGN.md、SPEC 文件、ADR
- API 规范:OpenAPI/Swagger、GraphQL 架构、.proto 文件
- 源代码文档字符串:JSDoc、Python 文档字符串、Go 文档注释、Rust
///注释
将输出保存为 {DOC_CONTEXT}。这些内容将用于:
- 阶段 1——确保切片分解参考文档中说明的行为和 API 契约
- 阶段 2——确保测试编写者编写的测试符合文档中的意图,而不只是代码签名
如果没有输出(未找到文档),也没有问题——不使用文档上下文,继续即可。
步骤 6:在项目根目录中创建状态文件 .tdd-state.json:
{
"feature": "user's feature description",
"framework": "jest|vitest|pytest|go|cargo|rspec|phpunit",
"language": "typescript|javascript|python|go|rust|ruby|php",
"test_command": "the full test command",
"source_dir": "src/",
"doc_context": "output from discover_docs.sh (or empty string)",
"auto_mode": false,
"dry_run": false,
"slices": [],
"current_slice": 0,
"phase": "setup",
"layer_map": {},
"files_modified": [],
"test_files_created": []
}
slices 数组中的每个切片都包含一个 layer 字段:"domain"、"domain-service"、"application" 或 "infrastructure"。有关如何分配层级,请参阅阶段 1。
layer_map 将目录前缀映射到层级。此映射在阶段 1 根据项目结构构建:
{
"layer_map": {
"src/domain/": "domain",
"src/services/": "domain-service",
"src/application/": "application",
"src/infrastructure/": "infrastructure",
"src/adapters/": "infrastructure",
"src/controllers/": "infrastructure"
}
}
如果项目没有明确的目录与层级映射(扁平结构),将 layer_map 设为 {},并跳过基于路径的验证。
步骤 5a(自动检测 layer_map):如果 layer_map 为空,请扫描源目录,查找常见的 DDD/分层架构目录名称,并自动填充:
Common directory → layer mappings (check if directories exist):
*/domain/ → "domain"
*/models/ → "domain" (ORM models often serve as domain entities)
*/entities/ → "domain"
*/value_objects/ → "domain"
*/services/ → "application" (unless clearly infrastructure)
*/application/ → "application"
*/use_cases/ → "application"
*/core/ → "application"
*/infrastructure/ → "infrastructure"
*/adapters/ → "infrastructure"
*/controllers/ → "infrastructure"
*/api/ → "infrastructure"
*/bot/ → "infrastructure" (Telegram/Discord bot handlers)
*/handlers/ → "infrastructure"
*/repositories/ → "infrastructure" (concrete repo implementations)
仅为源代码树中实际存在的目录添加条目。如果匹配的目录少于 2 个,则将 layer_map 留空(扁平项目)。将自动检测到的映射呈现给用户确认:
Auto-detected layer map from directory structure:
src/models/ → domain
src/services/ → application
src/core/ → application
src/bot/ → infrastructure
src/api/ → infrastructure
Does this mapping look correct? (adjust if needed)
更新状态:设置为 "phase": "setup"。立即写入状态文件。
阶段 1:规范分解
获取用户的功能需求,并将其分解为按顺序排列的垂直切片。每个切片对应一种可测试的行为。
使用文档上下文:分解时,请交叉参考阶段 0 步骤 5 中的 {DOC_CONTEXT}。文档通常会描述预期行为、边缘情况和 API 契约,这些内容有助于确定切片边界。如果文档提到了具体错误情况、验证规则或行为要求,请考虑将其作为切片候选。
从内到外排列切片
确定所有切片后,按架构层级从内到外排序。这样可确保每个切片都能基于之前切片中的真实(而非模拟)实现构建:
- 领域模型切片优先——纯逻辑,无依赖,无需模拟
- 领域服务切片——使用真实领域对象执行跨聚合操作
- 应用服务/用例切片——使用内存中的端口伪实现进行编排
- 基础设施适配器切片最后——存储库、外部 API、框架适配器
为每个切片分配一个 layer 标签:domain、domain-service、application 或 infrastructure。使用 references/layer_guide.md 中的启发式规则进行分类。
**为什么要从内到外?**领域切片会生成后续切片可直接使用的真实对象。这能最大限度地减少模拟,并尽早发现集成问题。此外,这也确保在做出任何基础设施决策之前,业务规则已经实现并经过测试。
对于所有代码都位于同一层的简单项目,所有切片的 layer 都设为 "application",排序保持不变——此指导原则会自然适配这类项目。
切片排序中的边界情况
仅涉及基础设施的功能(例如,“添加电子邮件提供商重试逻辑”“从 Postgres 切换到 MySQL”):
- 如果某项功能没有领域或应用层行为变更,则所有切片都可以标记为
infrastructure。这是有效的——可完全跳过内层。 - 呈现为:“这是纯基础设施变更。所有切片都属于基础设施层。”
缺少端口接口(领域服务需要一个尚不存在的端口):
- 第一个需要该端口的切片应在实现过程中创建此接口。实现者可以在内层创建文件(领域/领域服务可以定义自己的端口)。
- 示例:
RegistrationService的domain-service切片会在 GREEN 阶段创建domain/ports/UserRepository接口。
横切切片(切片涉及多个层):
- 标记为其涉及的最内层。实现者可以在该层及任何更内层创建文件。
- 示例:同时引入新领域事件的用例标记为
application,但会在domain/events/中创建文件。
向用户呈现:
I've broken this into N vertical slices (ordered inside-out):
Domain:
1. [behavior] — [what the test verifies]
Domain Services:
2. [behavior] — [what the test verifies]
Application:
3. [behavior] — [what the test verifies]
Infrastructure:
4. [behavior] — [what the test verifies]
Each slice follows RED -> GREEN -> REFACTOR before moving to the next.
Does this decomposition look right?
如果所有切片都属于同一层,则跳过层级标题,直接以平面列表呈现。
等待用户批准(即使在 --auto 模式下也必须如此——切片分解始终需要确认)。
更新状态:写入切片数组(每个切片都包含 layer 字段),并设置 "phase": "decomposed"。
Dry-Run 阶段覆盖(阶段 2–4)
在 --dry-run 模式下,完全替换阶段 2–4,对每个切片执行以下操作:
- 刷新 API 概览(
extract_api.sh) - 填入所有变量并渲染 Test Writer 提示词。在
### Test Writer Prompt (slice N)标题下打印。 - 使用占位测试代码渲染 Implementer 提示词:将
"(dry-run: test code would be generated by Test Writer)"用于{FAILING_TEST_CODE},将"(dry-run: no test output)"用于{TEST_FAILURE_OUTPUT}。 - 使用占位值渲染 Refactorer 提示词:将
"(dry-run: no green output)"用于{GREEN_TEST_OUTPUT},将"(dry-run: code from Test Writer)"用于{ALL_TEST_CODE},将"(dry-run: code from Implementer)"用于{ALL_IMPLEMENTATION_CODE}。 - 对每个渲染后的提示词,验证其中不再包含
{UNRESOLVED_VARIABLE}模式(正则表达式:\{[A-Z][A-Z_]+\})。将任何未解析变量报告为错误。 - 打印每个提示词的字符数。
- 继续处理下一个切片(不调用
Task()、不写入文件、不运行测试)。
处理完所有切片后,打印 dry-run 摘要并退出。不要清理状态文件——它可用于后续的 --resume。
阶段 2:RED —— 编写一个失败测试
步骤 1:刷新 API 概览(随着切片的实现,API 概览会发生变化):
bash ~/.claude/skills/tdd/scripts/extract_api.sh {SOURCE_DIR}
步骤 2:从 references/agent_prompts.md -> “Test Writer Agent” 部分读取提示词模板。填入以下内容来构建提示词:
{SLICE_SPEC}:当前切片的行为描述{LANGUAGE}:阶段 0 检测到的语言{FRAMEWORK}:检测到的框架名称{API_SURFACE}:extract_api.sh 的输出{DOC_CONTEXT}:discover_docs.sh 的输出(阶段 0 步骤 5)。仅包含与当前切片相关的部分——如果完整输出过大,则按关键词匹配筛选。{TEST_FILE_PATH}:测试文件应放置的位置(遵循项目约定){EXISTING_TEST_CONTENT}:测试文件的当前内容(如果存在),否则填写“尚无测试文件。”{FRAMEWORK_SKELETON}:references/framework_configs.md中相关的框架骨架{LAYER}:阶段 1 中该切片的层级标签{LAYER_TEST_CONSTRAINTS}:特定层的测试约束(参见 agent_prompts.md -> Layer-Specific Constraint Lookup)
步骤 3:启动 Test Writer Agent:
Task(subagent_type="general-purpose", prompt=<constructed prompt>)
步骤 4:使用 agent_prompts.md 中的 parse_agent_json 逻辑解析 JSON 响应:
- 如果存在 Markdown 围栏,则移除
- 尝试直接解析 JSON
- 如果失败,找到第一个
{和最后一个},尝试解析它们之间的子字符串 - 如果仍然无效:在 Task 调用后附加“Return ONLY a JSON object.”,然后重试一次
- 如果仍然失败:从原始响应中手动提取测试代码
步骤 5:将测试代码写入测试文件。如果文件已存在,则追加测试函数(并合并导入项)。如果是新文件,则使用 Agent 提供的 imports_needed + test_code 创建文件。
步骤 5a(写入后测试异味扫描):运行测试前,扫描测试代码中常见的异味:
| 异味 | 检测方式 | 操作 |
|---|---|---|
| 断言轮盘 | 同一测试函数中存在多个不带消息的裸 assert 语句(3 个或以上) | 警告用户(不阻止):“测试有 N 个裸断言——建议添加失败消息,以便更轻松地调试。” |
| 未知测试 | 测试名称过于笼统:匹配 test_1、test_it、test_works、test_example、test_thing | 重新启动 Test Writer,并附加:“使用描述性测试名称,使其读起来像行为规范(例如,test_rejects_empty_email)。” |
| 同义反复断言 | assert True、当函数不存在返回 None 的路径时使用 assert result is not None,或将 assert isinstance(result, X) 作为唯一断言 | 重新启动 Test Writer,并附加:“该断言是同义反复——请测试实际行为/值,而不只是函数是否返回了某个值。” |
步骤 5b(写入后分层检查):扫描测试代码中违反分层的模式:
| 层 | 测试代码中的禁止模式 |
|---|---|
domain | jest.mock(、vi.mock(、Mock(、mock.patch、unittest.mock、gomock、mockery — domain 测试不得使用模拟库 |
domain-service | domain 对象的相同模拟模式(模拟 ports/repos 可以) |
application | 无禁止模式(预期会模拟 ports) |
infrastructure | 无禁止模式 |
如果发现禁止模式:
- 从测试中移除违规模拟/模式
- 重新启动 Test Writer,并附加:“不要使用模拟库。这是一个 {LAYER} 层测试。请使用真实的 domain 对象。”
- 如果第二次尝试仍使用禁止模式,则询问用户
步骤 6:运行测试,确认其失败(预期是断言失败,而非设置错误):
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{TEST_COMMAND_FOR_SPECIFIC_TEST}"
步骤 7:使用语义验证评估结果:
| 结果 | 操作 |
|---|---|
status: "fail",断言错误 | 正确的 RED — 测试失败是因为预期行为尚不存在。继续。 |
status: "fail",ImportError / ModuleNotFoundError | 设置问题,不是正确的 RED。 测试甚至无法导入被测模块。修复:创建最小存根(空类/函数),使导入成功,然后重新运行。此时测试应因断言而失败。 |
status: "fail",缺少方法导致 AttributeError | 与导入错误类似 — 类存在,但方法不存在。如果断言也会失败,则这是可接受的 RED。继续。 |
status: "pass" | 行为已存在。记录:“测试通过 — 跳过该切片(已实现)。”递增 current_slice,移至下一个切片。 |
status: "error",SyntaxError | 修复:测试存在拼写错误。读取 raw_tail,直接修复测试文件。重新运行。如果尝试修复 2 次后仍然报错,则询问用户。 |
status: "error",编译/框架错误 | 修复:导入错误、缺少 fixture 或框架配置错误。读取 raw_tail,直接修复测试文件。重新运行。如果尝试修复 2 次后仍然报错,则询问用户。 |
步骤 8(仅交互模式 — 在 --auto 下跳过):向用户展示:
RED: Test written and failing as expected.
Test: {test_name}
File: {test_file_path}
Failure: {failure message from JSON}
This test verifies: {test_description from agent response}
Proceed to GREEN phase? (or adjust the test?)
继续执行 GREEN 前,请等待用户批准。
更新状态:"phase": "red",将测试文件添加到 test_files_created。立即写入状态。
阶段 3:GREEN — 最小实现
步骤 1:阅读失败的测试文件和测试失败输出(RED 阶段 run_tests.sh 结果中的完整 raw_tail)。
步骤 2:构建源文件(不含测试文件、node_modules 等)的文件树:
find {SOURCE_DIR} -type f \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.go' -o -name '*.rs' -o -name '*.rb' -o -name '*.php' \) | grep -v test | grep -v spec | grep -v node_modules | grep -v __pycache__ | grep -v vendor | grep -v target | grep -v dist | grep -v build | head -50
步骤 3:阅读测试导入或引用的现有源文件。
步骤 4:阅读 references/agent_prompts.md 中的提示词模板 → “Implementer Agent” 部分。填写:
{LANGUAGE}:检测到的语言{FAILING_TEST_CODE}:完整的测试文件内容{TEST_FAILURE_OUTPUT}:run_tests.shJSON 输出中的raw_tail{FILE_TREE}:步骤 2 中的源文件列表{EXISTING_SOURCE}:相关源文件的内容(如有;新建项目可能为空){LAYER}:该切片在 Phase 1 中的层标签{LAYER_DEPENDENCY_CONSTRAINT}:特定层的依赖约束(参见agent_prompts.md→ Layer-Specific Constraint Lookup)
重试时(attempt > 1),还要填写 {?PREVIOUS_ATTEMPT} 部分:
{PREVIOUS_ATTEMPT_DESCRIPTION}:失败尝试中的explanation字段{PREVIOUS_ATTEMPT_ERROR}:失败尝试后测试运行结果中的raw_tail
重要:不要包含切片规格、功能说明或任何未来计划。Implementer 仅依据测试进行工作。
步骤 5:启动 Implementer Agent:
Task(subagent_type="general-purpose", prompt=<constructed prompt>)
步骤 6:解析 JSON 响应。验证层边界,然后应用文件更改。
步骤 6a(层路径验证):如果 layer_map 不为空,请检查响应中的每个文件路径是否符合当前切片的层:
For each file in response.files:
inferred_layer = lookup file.path against layer_map (longest prefix match)
if inferred_layer exists AND inferred_layer != current_slice.layer:
if inferred_layer is OUTER relative to current_slice.layer:
REJECT: "Implementer created/modified {file.path} which belongs to
the {inferred_layer} layer, but this is a {current_slice.layer} slice.
Inner layers must not depend on outer layers."
→ Re-launch Implementer with appended constraint:
"Do NOT create or modify files in {inferred_layer} directories.
This slice is {current_slice.layer} only."
if inferred_layer is INNER relative to current_slice.layer:
ALLOW: outer layers may touch inner-layer files (e.g., adding a port interface)
“外层”检查的层级顺序:domain < domain-service < application < infrastructure。
如果 layer_map 为空(扁平项目),则跳过此验证。
步骤 6b:应用通过验证的文件更改:
对于响应 files 数组中的每个文件:
- 如果
action为"create"或"overwrite":使用 Write 工具创建或覆盖文件,并写入完整内容 - 如果
action为"edit"(用于超过 200 行的现有文件):使用 Edit 工具,通过old_string→new_string应用更改。Implementer 只会返回带有上下文的已更改函数——确定插入位置或要替换的函数,并相应地使用 Edit 工具。如果编辑目标不明确,则回退到读取完整文件并使用 Write。 - 对于超过 200 行且 Implementer 仍返回完整内容的现有文件(
action = "overwrite"),优先使用 Edit 工具仅应用差异,以避免意外重新格式化未更改的代码
步骤 7:运行指定测试:
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{TEST_COMMAND_FOR_SPECIFIC_TEST}"
步骤 8:重试循环(如果测试仍然失败):
attempt = 1
max_attempts = 5
previous_explanation = null
previous_error = null
while status != "pass" AND attempt <= max_attempts:
previous_explanation = explanation from last Implementer response
previous_error = raw_tail from last test run
Launch FRESH Task(Implementer) with:
- same test code + file tree + existing source (re-read!)
- NEW failure output
- PREVIOUS_ATTEMPT section filled in
Apply changes (Write tool for each file)
Re-run test
attempt += 1
if still failing after max_attempts:
STOP. Present to user:
"Implementation failed after 5 attempts. Last error: {raw_tail}"
Ask: "Adjust the test, try a different approach, or debug manually?"
每次重试都使用全新的 Task 调用,且只提供上一次尝试的说明和错误。这可以避免 Implementer 陷入无关问题,同时为其提供足够的信息来尝试不同的策略。
步骤 9:指定测试通过后,运行完整测试套件:
bash ~/.claude/skills/tdd/scripts/run_tests.sh {FRAMEWORK} "{FULL_TEST_COMMAND}" --all
步骤 10:处理回归问题:
| 结果 | 操作 |
|---|---|
| 全部通过 | 继续 REFACTOR |
| 发现回归问题 | 自动修复:使用回归测试失败信息启动全新的 Implementer。应用更改。重新运行完整测试套件。最多重复 3 次。如果修复 3 次后仍然失败,则停止并告知用户。 |
步骤 11(仅交互模式——在 --auto 模式下跳过):向用户展示:
GREEN: Test passing with minimal implementation.
Implementation: {explanation from agent response}
Files changed: {list}
All tests: {passed} passing, {failed} failing
Proceed to REFACTOR phase? (or adjust?)
更新状态:"phase": "green",更新 files_modified。立即写入状态。
步骤 12(仅适用于 domain/domain-service 切片):在 REFACTOR 之前进行层纯度检查:
对于 domain 或 domain-service 层切片中的每个新增/修改文件:
- 导入扫描:读取所有导入/require 语句。根据
layer_map检查每个导入的模块。将任何来自外层的导入标记为违规。 - 构造函数检查:确认构造函数不接受任何类型来自外层的参数(不允许 ORM 会话、HTTP 客户端、框架配置)
- 静态调用检查:不得静态调用外层代码中的方法
- 如果发现违规,请立即修复(将依赖移至端口接口),然后再进入 REFACTOR
第 13 步:全仓库导入扫描(所有层,每个切片运行一次):
扫描所有源文件(不限于本次会话修改的文件),检查依赖方向违规:
# For each source file, extract imports and check against layer_map
# Language-specific patterns:
# Python: from X import Y, import X
# TypeScript/JS: import ... from 'X', require('X')
# Go: import "X"
对于每个文件:
- 根据
layer_map确定其所属层(如果没有匹配项则跳过) - 对于每个导入,根据
layer_map确定被导入模块所属的层 - 如果被导入层相对于文件所属层位于外层 → 判定为违规
在进入 REFACTOR 前向用户报告违规:
Layer scan found N dependency direction violation(s):
- domain/user.py imports infrastructure/db.py (domain → infrastructure)
- domain/services/registration.py imports adapters/email.py (domain-service → infrastructure)
在 --auto 模式下:尝试自动修复(用端口接口替换具体导入)。在交互模式下:展示违规并询问用户如何处理。
此步骤补充了 Refactorer 的导入检查(它只检查会话文件),实现全仓库扫描。静态工具会漏掉约 23% 的违规(Pruijt et al., 2017)——结合文本与结构检查可提高覆盖率。
第 4 阶段:REFACTOR
第 1 步:收集所有上下文:
- 本次会话期间创建/修改的所有测试文件
- 本次会话期间修改的所有源文件
- 绿色测试输出
第 2 步:从 references/agent_prompts.md ->“Refactorer Agent”部分读取提示词模板。填写:
{LANGUAGE}:检测到的语言{GREEN_TEST_OUTPUT}:显示全部测试通过的完整测试输出{ALL_TEST_CODE}:所有测试文件的内容{ALL_IMPLEMENTATION_CODE}:所有已修改源文件的内容{SLICE_LAYERS}:截至目前已完成的所有切片中,不重复层名称的逗号分隔列表
第 3 步:启动 Refactorer Agent:
Task(subagent_type="general-purpose", prompt=<constructed prompt>)
第 4 步:解析 JSON 响应。如果 suggestions 为空,则跳至第 6 步。
按优先级顺序(优先级高的在前)逐条应用建议:
对于每条建议:
- 应用代码更改(使用 Edit 工具,为每个文件将
old_code替换为new_code) - 运行项目的 linter/formatter 检查(根据项目配置检测):
- Python:
python -m black --check {files} && python -m flake8 {files} && python -m mypy {files} - TypeScript/JS:
npx eslint {files}或npx tsc --noEmit - Go:
go vet ./... - Rust:
cargo clippy - 如果 lint 失败 -> 立即还原并跳过此建议(与测试失败处理相同)
- Python:
- 运行完整测试套件
- 如果任何测试失败 -> 立即还原(重新读取编辑前的文件并将其写回)并跳过此建议
- 如果所有测试和 lint 均通过 -> 保留更改
第 5 步(仅交互模式——在 --auto 模式下跳过):展示:
REFACTOR: Code improved, all tests still passing.
Applied: {list of accepted suggestions}
Skipped: {list of reverted suggestions, if any}
All tests: {count} passing
[Moving to slice N of M] or [All slices complete]
在 --auto 模式下,打印一行简短信息:
[auto] REFACTOR slice N/M: {applied_count} applied, {skipped_count} skipped
更新状态:"phase": "refactor"。立即写入状态。
第 5 阶段:下一个切片或完成
如果还有切片 -> 在状态中递增 current_slice,返回第 2 阶段。
如果所有切片均已完成 -> 展示摘要:
TDD Complete: {feature name}
Slices implemented: N
Tests written: N
Files created/modified: {list}
All tests passing: yes
清理:删除 .tdd-state.json(在 --auto 模式下静默删除;在交互模式下询问用户)。
恢复支持
用户调用 /tdd --resume 时:
- 从项目根目录读取
.tdd-state.json - 报告当前状态:“找到功能‘{feature}’的 TDD 会话。当前位于第 {N}/{total} 个切片,阶段:{phase}。”
- 从当前切片的当前阶段恢复
- 如果状态中的
auto_mode为 true,则继续使用自动模式
边缘情况
Greenfield 项目
没有源文件、没有测试、没有测试配置。请妥善处理:
- Phase 0 Step 3:如果 run_tests.sh 返回
status: "error"且total: 0,检查是否存在测试文件。如果没有,这就是一个 Greenfield 项目——继续执行。 - Phase 0 Step 4:extract_api.sh 会返回空输出。将
"(No existing API — this is a new project)"传给 Test Writer。 - Phase 2:Test Writer 会从头创建测试文件。可能需要设置测试框架配置(例如
jest.config.js、pytest.ini)。如果首次运行测试时因框架错误(而非测试失败)而失败,请创建最简框架配置并重试。
Bug 修复 TDD
- 编写一个能复现 Bug 的测试(应失败,以证明 Bug 存在)
- 确认失败与所报告的 Bug 相符——人工检查点
- 修复:只编写让测试通过所需的最少代码(按常规进入 GREEN 阶段)
- 验证:没有回归问题
现有代码(特征测试)
- 针对当前行为编写测试(应通过——这是特征测试)
- 修改测试以验证期望行为(应失败)
- 继续执行 GREEN -> REFACTOR
用户提供的测试
如果用户提供了测试代码:
- 运行测试,确认其失败(确认 RED)
- 跳到 Phase 3(GREEN)——以用户提供的测试为准
- 未经询问,不要修改测试
不稳定的测试
如果测试有时通过、有时失败:停止、报告,并在继续之前修复不稳定的测试。
失败恢复参考
| 失败 | 阶段 | 恢复方法 |
|---|---|---|
| Test Writer 返回无效 JSON | RED | 使用去除代码围栏 + 子字符串提取的方式解析。重试一次,并附上“Return ONLY JSON.”。最后手动提取。 |
| 本应失败的测试却通过了 | RED | 记录“已实现”,跳过当前切片,继续下一个。 |
| 测试有语法/编译错误 | RED | 阅读 raw_tail,直接修复测试文件。最多重试 2 次,然后询问用户。 |
| Implementer 返回无效 JSON | GREEN | 使用与 Test Writer 相同的 JSON 恢复方式。 |
| 实现后测试仍然失败 | GREEN | 重试循环:携带上一次尝试的上下文,重新调用 Implementer,最多 5 次。然后询问用户。 |
| 完整测试套件出现回归问题 | GREEN | 自动修复:使用回归失败信息重新调用 Implementer。最多尝试 3 次,然后询问用户。 |
| Refactorer 的建议导致测试失败 | REFACTOR | 立即还原,跳过该建议,继续下一个。 |
| run_tests.sh 超时 | 任意 | 增加超时时间。如果仍持续超时,询问用户测试性能方面的问题。 |
run_tests.sh 返回 "error" | 任意 | 阅读 raw_tail,查明原因。脚本错误(缺少二进制文件、路径错误)-> 修复并重试。编译错误 -> 视为实现错误。 |
| extract_api.sh 返回空 | RED | Greenfield 项目中的正常情况。传入“(No existing API)”消息。 |
| Agent 的回复完全为空 | 任意 | 重试一次 Task 调用。如果仍然为空,询问用户。 |
层参考
有关层的定义、依赖规则、各层的测试策略和检测启发式,请参阅 references/layer_guide.md。
应避免的反模式
请参阅 references/anti_patterns.md。重要事项:
- 绝不要为了让测试通过而修改测试(应修改实现,而非测试)
- 绝不要在编写测试之前编写实现
- 绝不要一次性编写所有测试(按纵向切片)
- 绝不要测试实现细节
- 绝不要跳过 RED 阶段
- 绝不要让领域代码导入基础设施(违反依赖方向)
- 绝不要模拟领域对象——应改为构造真实实例
框架快速参考
设置详情请参阅 references/framework_configs.md。
| 框架 | 运行单个测试 | 运行全部测试 | 监视模式 |
|---|---|---|---|
| Jest | npx jest --testPathPattern=<file> -t "<name>" | npx jest | npx jest --watch |
| Vitest | npx vitest run <file> -t "<name>" | npx vitest run | npx vitest |
| pytest | pytest <file>::<test_name> -v | pytest -v | pytest-watch |
| Go | go test -run <TestName> ./... | go test ./... | — |
| Cargo | cargo test <test_name> | cargo test | cargo watch -x test |
| RSpec | rspec <file>:<line> | rspec | guard |
| PHPUnit | phpunit --filter <test_name> | phpunit | — |