---
name: pdlc-test-setup
description: 立测试地基（探测技术栈 → 验证并生成 test-commands.yml → 脚手架测试目录 → 接本地钩子）
argument-hint: [项目目录] [--refresh] [--autonomous]
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
layer: 3
stage: engineering
artifact_type: surface
produces:
  - docs/00_standards/test-commands.yml
requires: []
next_step: null
terminal_state: null
recommended_model: sonnet
recommended_effort: medium
---

# 立测试地基

给项目一键立起「客观 check」的地基：**探测技术栈 → 逐条验证命令真能跑 → 写 `docs/00_standards/test-commands.yml` → 脚手架测试目录 → 接本地钩子**。

<!-- @include templates/prompts/iron-law-tool.md（已内联于下方，无需另读） -->
⛔ **IRON LAW · 不可违反的硬门禁（工具型命令）**

本命令不是主链路上的阶段：**不追加 `history`，不改 `current_stage` / `next_step` / `last_phase_result`**，也不为此分配功能ID。以下规则不可协商：

1. **文件必须落盘**：正文要求创建的文件必须作为实际文件写入磁盘，不可仅在对话中输出（只要求在对话里输出的，照正文办）。
2. **自检必须执行**：产出后按本命令的自检项逐项核对，不得以"已经很好了"为由跳过。
3. **防循环**：修复只做一次，不递归。修不了的问题记进报告，继续往下走。

**违反任一条 = 立即中止当前命令，输出违规详情，等待人工介入。**
<!-- @include-end templates/prompts/iron-law-tool.md -->
<!-- @include templates/prompts/noninteractive.md（已内联于下方，无需另读） -->
## 非交互模式（`--autonomous`）

若本命令的参数含 `--autonomous`，本命令进入**无人值守**模式，按以下规则处理原本需要人应答的交互点。**参数是唯一真源**：不带 `--autonomous` 即为交互模式，一切照旧正常询问用户；绝不回读状态机 `run_mode` 兜底（「掉出 autonomous」是安全的失败方向）。

1. **流程性确认**（如「测试已绿是否继续」「是否覆盖已有文件」）→ **不询问**，按预设默认前进，并把决策追加到状态机 `history[].auto_decisions[]`：
   ```json
   { "point": "<确认点描述>", "chose": "<所选默认>", "at": "<ISO 8601>" }
   ```
2. **真需人判断**（PRD 关键取舍、评审「需人工确认」项、真实循环依赖等无法安全默认的点）→ **不猜**：
   - `current_stage` 保持不变（不推进）
   - 写 `last_phase_result.ok = false` 且 `blocked_reason = "<原因>"`
   - 末行输出哨兵：`<<<PDLC blocked reason="<原因>">>>`
   - 立即结束命令，交还人类
   - **守卫拦截也走这一条**：前置守卫（缺设计文档、缺测试、测试没红等）在 `--autonomous` 下拦住本命令时，
     不能只输出 ⛔ 就停——同样写 `ok=false` + `blocked_reason`（写明哪条守卫、缺什么）并输出哨兵。
     否则状态机里仍是上一阶段的 `ok=true`，循环驱动只看到 `current_stage` 没动，会把「被拦」误报成「卡住」，真实原因丢失。
3. **破坏性操作**（发布 / 部署 / 打 tag / 触发 CI / DROP / force-push 等不可逆·外发操作）→ `--autonomous` **无效**，仍必须人工显式确认。
4. **顺手的 sidecar 产物**（如缺失时创建 `CHANGELOG.md`、补全文档 PDLC-TRACE 的创建时间等本阶段职责内、可安全默认的辅助改动）→ 视为流程性默认，**直接做并记入 `auto_decisions[]`**；这类改动不新增外部副作用，不属破坏性操作。

> 进入 autonomous 模式时，在状态机顶层写 `run_mode: "autonomous"` 仅供留痕（复盘区分人工 vs 循环产出）。
<!-- @include-end templates/prompts/noninteractive.md -->

## 为什么需要它

pdlc 的命门是「`checks` 只认命令退出码，绝不用模型自评」——`pdlc-tdd` / `pdlc-implement` / `pdlc-review`
与外层循环全都从 `docs/00_standards/test-commands.yml` 取命令。但**在此之前没有任何东西帮你把这个文件立起来**，
没有它，整条客观化链路就是空的。本命令把这块地基变成 turnkey。

> ⛔ **本命令最重要的一条纪律**：**写进 `test-commands.yml` 的每条命令，必须先被真跑过一次、亲眼看到退出码。**
> 一条"看起来对但跑不了"的命令**比留空更坏**——它会让下游每个阶段都拿到假的 `checks`，
> 而整个 pdlc 的可信度正建立在这些 checks 是真的之上。**猜出来的命令一律不写。**

## `--refresh`：让这份 yml 跟上项目的演进

项目会漂移——脚本改名、runner 换代、工具从依赖里移除、子项目增删。这份 yml 一旦过期，
下游所有 `checks` 就开始失真。`--refresh` 是**重新探测 + 给出 diff**，而不是从头再来：

1. **逐条复跑现有命令**，按 `check-commands.md` 的三态判定谁还活着（跑不通 ≠ 检查没过）。
2. **重新探测候选**，与现状对比，得出变更清单。
3. **按方向决定自不自动**（这条是安全底线）：

<!-- @include templates/prompts/check-commands.md（已内联于下方，无需另读） -->
## 跑 check 命令：退出码的三态语义

**用脚本跑，不要手写 `checks`**：

```bash
bash scripts/pdlc-checks.sh --only <本阶段要的项，如 unit,lint> <项目根>
```

脚本是本 skill 自带的 `scripts/pdlc-checks.sh`。它读 `docs/00_standards/test-commands.yml`（唯一真源），逐条真跑，按下表映射，**stdout 只有一行 JSON**——
**原样**写进 `last_phase_result.checks`，不改键名、不改值、不补不删。每条命令的退出码与输出末尾回显在
stderr，报告里引用那几行即可。脚本退出码 `2`（没有 yml 等）= 没有可跑的 check → `checks: {}`。

> 为什么不手写：键名、布尔类型、三态这几件事是确定性的，交给模型写，真机上同一份项目轮与轮之间就换一种错法
> （键名照抄 yml 的 `unit`、值写成 `"4 passed, 1 failed"`、`127` 写成 `false`）。
> 环境里没有 `bash` 时才按下表手工映射。

脚本按退出码分三态——不是两态。这是 IRON LAW「checks 只认客观事实」在执行层的落法：

| 观察到的 | 含义 | 写进 `checks` |
|---|---|---|
| 退出码 `0` | 通过 | 对应键 = `true` |
| 退出码非 0（命令**跑起来了**，只是没过） | 未通过 | 对应键 = `false` |
| 退出码 `127` / `command not found` / 脚本文件不存在 / 该项为空字符串 | **无法判定** | **省略该键，或写 `null`**——**绝不能是 `false`** |

> ⛔ **唯一的红线是不许写 `false`**：那是**会误导人的虚报**——它说的是"检查失败了"，
> 于是有人去查代码，但真正的问题是**配置过期**，代码可能完全没毛病。
>
> **省略键与 `null` 等价，两种都可以**：对消费方而言无法区分（`jq '.checks.lint_clean'`
> 在两种情况下都返回 `null`）。`null` 甚至更明确——省略是歧义的（"没看"还是"看了判不出"），
> `null` 明说"看了，判不出"。**别在这上面纠结，力气花在不写 `false` 上。**
>
> 这与「没有检查命令可跑的阶段 → `checks: {}`」同源。

## 「跑不了」＝ `test-commands.yml` 过期信号（顺带检测，零额外成本）

命令跑不起来，几乎总意味着**这份 yml 已经跟不上项目了**——脚本改名、runner 换了、
工具从依赖里移除、子项目路径调整。真实项目里这类漂移是常态（例如某前端框架升级后
移除了内置 lint 子命令，而 yml 里那条命令还在）。

由于**各阶段本来就在跑这些命令**，这个信号是白捡的。检测到时：

1. 在本阶段的报告里单列一条：**「`test-commands.yml` 疑似过期」**，写明是哪一项、
   观察到什么（退出码 / 报错原文）、以及为什么判定为"跑不了"而非"没通过"。
2. 提示补救：`/pdlc-test-setup --refresh`（重新探测并给出 diff）。
3. **不要自作主张改 yml**——本阶段的职责是干活，不是改配置；只报告，不动手。

## 变更方向决定自动化程度（`--refresh` 时适用）

更新这份 yml 等于**改变"通过"的定义**，所以按**方向**区别对待：

| 方向 | 例子 | 处理 |
|---|---|---|
| **让闸门变严** | 空着的 `e2e` 现在能跑了、覆盖率阈值上调 | **可自动应用**，报告留痕 |
| **平移替换** | 命令改名但语义相同，且新命令**已验证能跑** | **可自动应用**，报告留痕 |
| **让闸门变松** | 删掉某条 check、把命令改成空、下调阈值 | **必须人确认**，绝不自动 |

> ⚠️ 这条方向规则是防「自动修复把闸门修没了」：lint 命令坏掉时，**把它留空**是最省事的
> "修法"，结果闸门悄悄松了、报告还是绿的——比不更新更危险。
> **变严可以自动，变松必须由人签字。**
<!-- @include-end templates/prompts/check-commands.md -->

4. **不管自动与否，全部变更都要在报告里列出**：改了什么、为什么、依据是哪次真跑的退出码。
   自动应用的也要能一眼看出来，便于事后 `git diff` 复核。

> ⚠️ **最危险的"自动修复"是把坏掉的 check 留空**——闸门瞬间松了，报告还是绿的。
> 所以留空 / 删除 / 降阈值 / **替换**一律走人工确认，`--autonomous` 也不豁免。
>
> **替换也算**，哪怕换上去的命令能跑、看起来更严：原命令的规则往往已不可考，没法证明新命令与它等价——
> 新命令可能在这一维更严、在另一维更松。**方向不可判，就不能当作变严自动应用**（与三态「查不了 ≠ 没问题」
> 同一条纪律）。能自动应用的只有一种：原来**留空**的项，这次探测到命令并真跑验证通过——那是从「没测」到
> 「测了」，方向确定。

**从哪来的过期信号**：不用你盯着——`pdlc-tdd` / `pdlc-implement` / `pdlc-review` 每次跑 check
时遇到"命令跑不了"都会提示，`/pdlc-quality` 的报告里还有专门的「配置健康度」一节。
看到提示再来 `--refresh` 即可。

## 段一：探测与验证

### 1.1 技术栈探测

扫描特征文件，识别语言 / 包管理器 / 测试框架：

| 特征文件 | 栈 | 典型 unit | 典型 coverage | 典型 lint |
|---|---|---|---|---|
| `Cargo.toml` | Rust | `cargo test` | `cargo llvm-cov --fail-under-lines <阈值>` | `cargo clippy -- -D warnings` |
| `package.json` | Node | `pnpm test` / `npm test` | `vitest run --coverage.thresholds.lines=<阈值>` | `npx eslint .` |
| `pyproject.toml` / `requirements.txt` | Python | `pytest` | `pytest --cov --cov-fail-under=<阈值>` | `ruff check .` |
| `go.mod` | Go | `go test ./...` | `go test ./... -cover` | `golangci-lint run` |
| `pom.xml` / `build.gradle` | JVM | `mvn test` / `./gradlew test` | jacoco check | `mvn checkstyle:check` |
| 仅 `*.sh` | Shell | 项目自有测试脚本 | —（通常无） | `shellcheck <文件>` |

**多语言 / monorepo**：逐个子项目探测；`test-commands.yml` 只能有一组命令，所以要么用能覆盖全仓的聚合命令
（如 `pnpm -r test`），要么与用户确认以哪个子项目为准。**探测不到唯一答案时不要自己拍板**（见 §1.3）。

### 1.2 逐条验证（不可跳过）

对每个候选命令**真的跑一次**，按退出码归类：

| 观察到的结果 | 结论 | 动作 |
|---|---|---|
| 退出码 0 | 命令可用且当前通过 | **采纳** |
| 退出码非 0、非 127，且输出像测试/lint 报告 | 命令可用，只是当前有失败项 | **采纳**（地基是"命令能跑"，不是"当前全绿"） |
| 退出码 127 / `command not found` / 工具未安装 | 命令不可用 | **留空**，在报告里写明缺什么 |
| 无对应配置（如没配覆盖率工具） | 该项本项目暂无 | **留空** |
| 命令挂起 / 需要交互 | 不适合做自动 check | **留空**，报告里说明 |

> ⚠️ **留空是合法且诚实的结果**，与状态机里「没有检查命令可跑的阶段 → `checks: {}` 留空」同一条纪律。
> 宁可空着并在报告里提示怎么补，也不要写一条没验证过的命令。

**覆盖率达标线写死在命令参数里**（如 `--cov-fail-under=85`），不做二次解释——这样"达标"就是退出码本身，
不需要任何一方去解析百分比数字。默认阈值 **85%**；项目已有更高要求则沿用已有。

### 1.3 需要人拍板的点（`--autonomous` 下 block，不猜）

以下属判断题而非流程题，**不得自动选**，须写明原因交还人类：

- 探测到**多个**并列候选（如同时有 `jest` 和 `vitest` 配置），无法判定以哪个为准
- **零候选**（项目还没有任何测试框架）——装哪个框架是技术选型，必须人定
- monorepo 里以哪个子项目 / 哪条聚合命令为准
- 覆盖率阈值定多少（若项目无既有约定）

探测到**唯一**候选且验证通过 → 属流程性确认，`--autonomous` 下自动采纳并在报告里留痕。

## 段二：落地

### 2.1 写 `docs/00_standards/test-commands.yml`

以 本 skill 目录下的 `assets/test-commands-template.yml` 为骨架。**这是 surface 型产物**——就地编辑，不做 `-v2` 累积。

- **文件已存在** → **不覆盖**。改为逐条校验现有命令是否仍能跑：
  - 仍能跑 → 保持原样（用户的选择优先于探测结果）
  - 已跑不通（工具改名 / 脚本删了）→ 报告里列出，**建议**改法，等人确认。`--autonomous` 下同样**不改原值**——
    建议的替换命令哪怕已真跑通过，也只写进报告的「⚠️ 待人工」，yml 里保持原样
  - 缺失的项（空字符串）→ 若这次探测到可用命令，提议补上
- **文件不存在** → 用本次验证通过的命令生成；未验证通过的项留空字符串。

### 2.2 脚手架测试目录（已有则不动）

按栈惯例建空目录 + 一个占位说明，**不生成业务测试用例**：

- Rust `tests/`、Node `src/__tests__/` 或 `tests/`、Python `tests/`、Go 同包 `*_test.go`、JVM `src/test/java/`
- **跟随项目既有布局**，不新造平行目录。守卫侧（`/pdlc-tdd`、`/pdlc-implement`）的测试定位规则
  是布局无关的，所以这里不必迁就任何预设结构

### 2.3 接本地钩子（不进 CI）

在**本地** git 钩子里跑基础 check（`husky` / `lefthook` / `pre-commit` / 原生 `.git/hooks`，按项目已有的来）：

- **pre-commit**：`lint`（快，秒级）
- **pre-push**：`unit`（+ `coverage` 若已配）

> **不新建 CI workflow**：这些 check 本地秒级可得，放 CI 只会让每次迭代都烧配额。
> 已有 CI 的项目也不改它的触发条件——那需要项目所有者单独授权。

### 2.4 老项目：可选的轻量底线回填

仅当用户要求：为**当前覆盖率最低**的若干核心模块补特征化测试（characterization test，锁住现有行为），
把覆盖率抬到阈值线。**这不是补齐测试**，只是让地基能立住。深度用例仍走 `/pdlc-tdd`。

## 段三：自检（强制）

<!-- @include templates/prompts/self-audit.md（已内联于下方，无需另读） -->
## 段二：自检（强制）

重新阅读本次产出物，按质量关卡清单逐项检查。勾选已通过，标注未通过原因。

> **注意**：自检清单的具体内容由各命令自行定义，本片段只规定结构。

## 段三：修复（单次，不递归）

针对自检段标注为未通过的项：

- **可自动修复**：直接修复（如补缺字段、修正格式、补齐缺失段落）
- **修复后回验**：再次运行自检，确认被修复项现在通过
- **无法自动修复**：记录到自审报告，不再尝试，流程继续

⚠️ 单次修复原则：若一轮修复后仍有项未通过，**不再递归修复**，防止死循环。
<!-- @include-end templates/prompts/self-audit.md -->

### 自检清单（必须全部检查）

- [ ] `test-commands.yml` 里**每一条非空命令**，都在本次会话中被真跑过、看到过退出码
- [ ] **收尾复跑一遍**：从写好的 yml 里逐条读命令再跑一次，确认与写入时的结论一致（防止写错路径 / 引号）
- [ ] 留空的项，报告里都写明了「为什么空」和「怎么补」
- [ ] 覆盖率阈值已写死在命令参数里，不依赖任何一方解析百分比
- [ ] 测试目录跟随项目既有布局；若写了 `test-commands.yml`，其 `unit` 命令能定位到这些测试
- [ ] 钩子是**本地**的，没有新建或修改任何 CI workflow
- [ ] 已存在的 `test-commands.yml` 没有被静默覆盖
- [ ] （`--refresh` 时）所有变更已在报告里列出；**没有任何"让闸门变松"的改动被自动应用**
- [ ] （`--refresh` 时）已存在但跑不通的命令**原值未动**；替换方案只写在报告里等人确认，没有以「变严」为由自动替换

## 段四：修复（单次，不递归）

<!-- @include templates/prompts/loop-prevention.md（已内联于下方，无需另读） -->
## 防循环规则

本命令所有的自检-修复循环均受以下约束：

1. **单次检查**：同一个自检清单在本次命令执行中只跑一次（起始 + 修复后验证共两次读）
2. **单次修复**：发现的问题只尝试修复一轮
3. **不递归**：修复后不再重新触发自检的全量重跑
4. **失败降级**：无法自动修复的问题 → 记录到自审报告 → 流程继续 → 最终报告标注待人工处理

这是为了防止 agent 在"修完再查、查完再修"的往返中陷入死循环。
<!-- @include-end templates/prompts/loop-prevention.md -->

- 复跑发现某条命令与写入时结论不一致 → 修正或改为留空
- 无法自动修复 → 记入报告，交还人类

## 段五：交接

<!-- @include templates/prompts/handoff.md（已内联于下方，无需另读） -->
## 段四：交接（Handoff）

命令完成后必须输出以下格式的最终消息：

```
✅ <阶段名> 完成：<主要产出物路径>
📊 自检：<通过数>/<总数> 通过（若有未通过，附要点）
📦 状态快照：docs/.pdlc-state/<feature-id>.json
👉 下一步：/pdlc-<next_step>
   （如果有分叉）或 /pdlc-<alt>（条件：<选择依据>）
```

**规则：**
- 主流程命令（写状态机的命令；下一跳见正文里「本命令的状态机取值」）必须显式输出"下一步"，不可省略
- 工具型命令（Layer 3）可以没有 `next_step`，此时输出 `👉 下一步：（本次流程结束，无后续）`
- 分叉场景必须说明**选择条件**，例如"若需补充测试用例 → `/pdlc-tdd`；若测试已齐 → `/pdlc-review`"
<!-- @include-end templates/prompts/handoff.md -->

**本命令的 handoff 输出：**

```
✅ 测试地基已立：docs/00_standards/test-commands.yml
  unit     : <命令>            （退出码 <N>，已验证）
  coverage : <命令 | 留空>      （<验证结论 | 为什么空>）
  lint     : <命令>            （退出码 <N>，已验证）
  e2e      : <命令 | 留空>      （<验证结论 | 为什么空>）
🪝 本地钩子：pre-commit → lint · pre-push → unit
📁 测试目录：<路径列表>
⚠️ 待人工：<留空项怎么补 / 需要拍板的选型>
👉 下一步：/pdlc-tdd <功能描述>   —— 本命令只立地基，深度用例走 TDD
```

## 诚实边界（务必如实说明，不要夸大）

- 本命令**只立地基 + 可选补底线**，**不生成完整测试套件**。AI 生成的测试容易浅、容易只测 happy path，
  真正的用例设计仍走 `/pdlc-tdd`（测试先行、红灯门）。
- 留空的项就是**当前没有**，不要为了让输出好看而填一条没验证过的命令。
- 覆盖率阈值只是一条线，**过线不等于测得好**——它挡的是"几乎没测"，不保证用例有效。

---

**目标项目**: $ARGUMENTS
