---
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.md -->
<!-- @include 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 -->

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`

以 `templates/test-commands-template.yml` 为骨架。**这是 surface 型产物**——就地编辑，不做 `-v2` 累积。

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

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

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

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

### 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 -->

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

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

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

<!-- @include templates/prompts/loop-prevention.md -->

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

## 段五：交接

<!-- @include 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
