---
name: forge-qa
description: |
  QA 验收与测试报告。纯验收模式：测试+报告，不修代码。
  两种调用模式：
    Mode A（完整 QA）：test-spec 生成 → 10 维度 Playwright 断言引擎 → 智能分析。
    Mode B（单 bug 修复回归）：配合 forge-bugfix 的 P6 调用，读取 docs/bugfix/reviews/BF-XX.md，
      针对 Bug 修复验收报告里的人工验收指南跑自动化测试，把逐步截图、深度断言、
      前后端环境身份校验回填到报告。QA 全过 → 单 bug 模式进 P6.5，批量模式进入
      qa-pass-pending-final-review；QA 有挂 → 通知 forge-bugfix 有界回 P5。
  核心原则：断言引擎模式，每个测试必须有 pass/fail，不允许 catch 吞错误；
  浏览器验收必须使用调用方传入或 dev-status 输出的 app_url，不猜 localhost 端口。
  在 Codex 环境中，如果 Browser Use 插件可用，前端页面/交互验收优先使用
  browser-use:browser 的 Codex in-app browser 采集用户视角截图和 DOM 证据；
  Computer Use 只作为 browser-use 不可用或非浏览器桌面应用场景的兜底。
  在功能开发后的 QA 自动闭环中，forge-qa 发现 bug 时必须产出结构化 bug 信息，
  供 forge-bugfix 创建 BF 报告并独立 worktree/TDD 修复。
  支持多种测试引擎：browser-use:browser、Playwright、gstack/browse、纯代码。
  触发方式：
    Mode A：用户说"测试"、"QA"、"forge-qa"、forge-dev 调度器调用
    Mode B：forge-bugfix 的 P6 调用（传入 review_doc 路径）
allowed-tools:
  - Bash
  - Read
  - Write
  - Edit
  - Glob
  - Grep
  - AskUserQuestion
---

# /forge-qa：QA 验收与测试报告

**纯验收模式：测试 + 报告，不修代码。** 发现的问题生成结构化 bug 记录；单 bug 回归时回填 Bug 修复验收报告。

## 调用模式

forge-qa 支持两种调用模式，**入口判断在前置脚本阶段**完成（见"前置脚本"节）。

| 模式 | 触发条件 | 输入 | 输出 | 下游 |
|---|---|---|---|---|
| **Mode A：完整 QA** | 用户直接触发，或 forge-dev 调度 | PRD / DESIGN / git diff | QA.md 报告 + 结构化 bug 候选 + User Gate | forge-ship / forge-bugfix / forge-eng |
| **Mode B：单 bug 修复验收报告** | forge-bugfix 的 P6 调用；入口带参数 `review_doc=docs/bugfix/reviews/BF-XX.md` | 单 bug Bug 修复验收报告 | 报告内 QA 证据区、环境身份校验、逐步截图回填 | forge-bugfix 的 P6.5 / 批次最终验收 / P5（回修）|

**模式判断逻辑**（前置脚本执行）：

**模式判断优先级**（从高到低）：

1. **显式参数** — Skill 调用时 args 含 `mode=B` 和 `review_doc=<路径>` → 直接 Mode B
2. **调用来源** — 触发消息里出现 "forge-bugfix"、"review-checklist"、`BF-\d+-\d+.md` 文件路径 → Mode B
3. **默认** — Mode A

```bash
# AI 从 args 或触发消息中解析
# 优先级 1: 显式 args
if echo "$ARGS" | grep -q "mode=B"; then
  MODE="B"
  REVIEW_DOC=$(echo "$ARGS" | grep -oE "review_doc=[^ ]+" | cut -d= -f2)
  BUG_ID=$(echo "$ARGS" | grep -oE "bug_id=[^ ]+" | cut -d= -f2)
  WORKTREE=$(echo "$ARGS" | grep -oE "worktree=[^ ]+" | cut -d= -f2)
  COMMIT=$(echo "$ARGS" | grep -oE "commit=[^ ]+" | cut -d= -f2)
  APP_URL=$(echo "$ARGS" | grep -oE "app_url=[^ ]+" | cut -d= -f2)
  echo "QA Mode: B（单 bug 修复验收报告）"
  echo "  review_doc: $REVIEW_DOC"
  echo "  bug_id: $BUG_ID"
  echo "  worktree: $WORKTREE"
  echo "  commit: $COMMIT"
  [ -n "$APP_URL" ] && echo "  app_url: $APP_URL"
# 优先级 2: 启发式识别
elif echo "$USER_MESSAGE" | grep -qE "forge-bugfix|review-checklist|docs/bugfix/reviews/BF-[0-9]+-[0-9]+\.md"; then
  MODE="B"
  # 从消息里捞 review_doc 路径
  REVIEW_DOC=$(echo "$USER_MESSAGE" | grep -oE "docs/bugfix/reviews/BF-[0-9]+-[0-9]+\.md" | head -1)
  echo "QA Mode: B（启发式判定）"
  echo "  review_doc: $REVIEW_DOC"
  # AI 必须验证：报告存在 + 其他必需参数从报告或上下文推断
else
  MODE="A"
  echo "QA Mode: A（完整 QA）"
fi

# Mode B 必需参数校验
if [ "$MODE" = "B" ]; then
  [ -f "$REVIEW_DOC" ] || { echo "❌ Bug 修复验收报告不存在: $REVIEW_DOC"; exit 1; }
  [ -n "$BUG_ID" ] || BUG_ID=$(basename "$REVIEW_DOC" .md)
  if [ -n "$WORKTREE" ] && [ -d "$WORKTREE" ] && [ -z "$APP_URL" ]; then
    if [ -f "$WORKTREE/package.json" ] && (cd "$WORKTREE" && npm run 2>/dev/null | grep -q "dev:status"); then
      echo "⚠️ 当前项目提供 dev:status，但 Mode B 未传 app_url。若验收项涉及浏览器、curl 或截图，调用方必须先运行 npm run dev:status，并把 Frontend URL 作为 app_url 传入。"
    elif [ -x "$WORKTREE/scripts/dev-stack.sh" ]; then
      echo "⚠️ 当前项目提供 scripts/dev-stack.sh，但 Mode B 未传 app_url。若验收项涉及浏览器、curl 或截图，调用方必须先运行 dev-stack status，并把 Frontend URL 作为 app_url 传入。"
    fi
  fi
fi
```

Mode B 详见"## Mode B：单 bug 修复验收报告模式"节（本文档末尾）。

Mode A 详见"## 三层架构"往下的完整流程。

**Mode B 的 args 契约（forge-bugfix 必须传，forge-qa 必须接收）**：

| 参数 | 必填 | 含义 |
|---|---|---|
| `mode=B` | ✅ | 强制信号，优先级最高 |
| `review_doc=<路径>` | ✅ | Bug 修复验收报告（存在性校验失败直接 exit） |
| `bug_id=BF-{MMDD}-{N}` | ✅ | 用于命名截图 / 日志 |
| `worktree=<路径>` | ✅ | 在该 worktree 内运行测试 |
| `commit=<hash>` | ✅ | 用于定位修复范围 |
| `app_url=<URL>` | 条件 | 仅当 bug 类型涉及应用运行时；必须来自调用方的 `dev:status` / `dev-stack status` 输出 |

## 三层架构

```
┌─────────────────────────────────────────────────┐
│  Layer 1: 测试规格生成（文档 → test-spec.json）     │
│  输入: PRD / DESIGN.md / git diff / 会话上下文       │
├─────────────────────────────────────────────────┤
│  Layer 2: 10 维度 Playwright 断言引擎               │
│  控制台|数据驱动|网络|视觉|交互|响应式|可访问|SSE|URL|懒加载│
├─────────────────────────────────────────────────┤
│  Layer 3: 智能分析（失败归因 + 根因定位）            │
│  console → 源码 → git diff 交叉引用                │
└─────────────────────────────────────────────────┘
```

## 铁律

1. **只测不修** — forge-qa 不修改任何业务代码。发现 bug 记录到报告，由 forge-eng 修复。
2. **不生成 test-spec 就不执行测试** — 先从文档提取验收项，结构化后再执行。
3. **每个测试必须有 pass/fail** — 不允许 `.catch(() => {})` 吞错误，不允许"只截图不断言"。
4. **断言必须验证功能正确性，不能只验证元素存在** — `visible` 和 `count_gte` 是前置条件，不是验收断言。每个测试用例必须至少包含一个验证**数据值/文本内容/状态变化**的深层断言（`contains_text`、`has_attribute`、`css_value`、`matches_regex`、自定义 `evaluate`）。详见下方"断言深度规则"。
5. **证据先于结论** — 每个测试结果必须有截图、输出、或日志作为证据。
6. **控制台零容忍** — 任何 `pageerror` 或 `console.error` 自动 FAIL。
7. **不得猜本地端口** — 有 `app_url` 就只测该 URL；没有 `app_url` 时，优先读取 `dev:status` / `dev-stack status`，不得自行发明 `localhost:3000`、`5173`、`8080` 等地址。
8. **Codex 浏览器优先** — 在 Codex 中做本地前端页面/交互 QA 时，若 Browser Use 插件可用，优先使用 `browser-use:browser`。不得因为 Computer Use 工具可见就跳过 Browser Use；Computer Use 只作明确兜底。

## 定位说明

| forge-eng 负责 | forge-qa 负责 |
|----------------|--------------|
| 单元测试（TDD 红绿重构） | **端到端用户流程测试** |
| 原子 commit 验证（exit code） | **跨模块集成测试** |
| 任务级验证 | **7 维度断言（视觉+响应式+可访问性+网络+数据驱动）** |
| — | **验收标准逐项核对** |
| — | **User Gate（用户验收关卡）** |

## 完整流程

```
第0步 上下文探测
  ├── 0.1 Worktree 检测
  ├── 0.2 文档链定位（PRD/DESIGN/ENGINEERING/FEEDBACK）
  ├── 0.3 变更范围分析（git diff）
  ├── 0.4 选择器审计（铁律：不盲猜选择器）
  └── 0.5 测试级别确认
  │
第1步 建立健康基准
  │
  ├── 已有 QA.md → 第2步 理解现状
  └── 无 QA.md   → 第2步(替代) 从零创建
  │
第2.5步 生成 test-spec（铁律：不生成就不执行）
  │
第3步 测试计划确认（用户审查 test-spec 摘要）
  │
第4步 更新 QA 文档
  │
第5步 10 维度测试执行
  ├── Phase 1: 控制台[console] + 网络[network]
  ├── Phase 2: 交互[functional] + 数据驱动[data-driven] + SSE[streaming] + URL状态[url-state] + 懒加载[async-content]
  ├── Phase 3: 视觉[visual] + 响应式[responsive]
  └── Phase 4: 可访问性[accessibility]
  │
第6步 智能分析 + Bug 报告
  │
第7步 User Gate（用户验收 — 不可跳过）
  │
  ├── accept → forge-ship
  └── reject → FEEDBACK.md → forge-eng → forge-qa (回归) → User Gate
```

全程中文。关键测试策略需用户确认后再执行。

## 报告产出后的出口

```
QA 验收完成。下一步：

[全部通过 + 用户验收通过]
→ /forge-ship 或 /forge-review

[有 FAIL 或用户 reject]
→ 生成修复清单 + FEEDBACK.md → /forge-eng 修复 → /forge-qa 回归
```

---

## 前置脚本

```bash
_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
echo "当前分支: $_BRANCH"

# === Worktree 检测 ===
_IN_WORKTREE="no"
_WORKTREE_ROOT=""
git worktree list 2>/dev/null | while read line; do
  echo "  worktree: $line"
done
[ "$(git rev-parse --git-common-dir 2>/dev/null)" != "$(git rev-parse --git-dir 2>/dev/null)" ] && _IN_WORKTREE="yes" && _WORKTREE_ROOT="$_ROOT"
echo "在 Worktree 中: $_IN_WORKTREE"

# === 测试引擎 1: gstack/browse ===
B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/browse/dist/browse" ] && B="$_ROOT/.claude/skills/gstack/browse/dist/browse"
[ -z "$B" ] && [ -x "$HOME/.claude/skills/gstack/browse/dist/browse" ] && B="$HOME/.claude/skills/gstack/browse/dist/browse"
[ -n "$B" ] && echo "gstack/browse: $B" || echo "gstack/browse: 不可用"

# === 测试引擎 2: Playwright ===
PW=""
command -v npx >/dev/null 2>&1 && npx playwright --version >/dev/null 2>&1 && PW="npx"
[ -z "$PW" ] && python3 -c "from playwright.sync_api import sync_playwright" 2>/dev/null && PW="python"
[ -n "$PW" ] && echo "Playwright: 可用 ($PW)" || echo "Playwright: 不可用"

# === qa-runner.mjs 检测 ===
QA_RUNNER=""
[ -f "$HOME/.claude/skills/forge-qa/scripts/qa-runner.mjs" ] && QA_RUNNER="$HOME/.claude/skills/forge-qa/scripts/qa-runner.mjs"
[ -n "$QA_RUNNER" ] && echo "qa-runner: $QA_RUNNER" || echo "qa-runner: 不可用"

# === 框架检测 ===
[ -f "$_ROOT/package.json" ] && grep -q '"react"' "$_ROOT/package.json" 2>/dev/null && echo "框架: React"
[ -f "$_ROOT/package.json" ] && grep -q '"vue"' "$_ROOT/package.json" 2>/dev/null && echo "框架: Vue"
[ -f "$_ROOT/package.json" ] && grep -q '"next"' "$_ROOT/package.json" 2>/dev/null && echo "框架: Next.js"
[ -f "$_ROOT/requirements.txt" ] || [ -f "$_ROOT/pyproject.toml" ] && echo "运行时: Python"
[ -f "$_ROOT/package.json" ] && echo "运行时: Node.js"

# === 本地服务探测 ===
echo "本地服务:"
if [ -n "$APP_URL" ]; then
  echo "  APP_URL=$APP_URL（由调用方传入）"
elif [ -f "$_ROOT/package.json" ] && (cd "$_ROOT" && npm run 2>/dev/null | grep -q "dev:status"); then
  (cd "$_ROOT" && npm run dev:status)
  echo "  未传 APP_URL：如需浏览器验收，请使用 dev:status 输出中的 Frontend URL 重新调用 forge-qa。"
elif [ -x "$_ROOT/scripts/dev-stack.sh" ]; then
  (cd "$_ROOT" && bash scripts/dev-stack.sh status)
  echo "  未传 APP_URL：如需浏览器验收，请使用 dev-stack status 输出中的 Frontend URL 重新调用 forge-qa。"
else
  for port in 3000 3456 4000 5173 8080 8081; do
    curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port" 2>/dev/null | grep -qE "200|301|302|304" && echo "  http://localhost:$port ✓（旧项目兜底探测）"
  done
fi

# === 报告目录 ===
REPORT_DIR="$_ROOT/.gstack/qa-reports"
mkdir -p "$REPORT_DIR/screenshots" 2>/dev/null
echo "报告目录: $REPORT_DIR"
```

---

## AskUserQuestion 格式规范

每次提问结构：
1. **重新聚焦**：当前项目、分支、正在测试的功能
2. **通俗解释**：高中生能懂的语言描述问题
3. **给出建议**：推荐选项 + 完整度评分
4. **列出选项**：`A) B) C)` + 工作量估算

---

## 第0步：上下文探测与环境准备

### 0.1 Worktree 检测（铁律：在正确的分支上测试）

按优先级检测工作环境：

1. **forge-dev 调度传入**：如果 Agent prompt 中包含 `worktree_path`，直接 `cd` 进入
2. **当前目录检测**：前置脚本已检测 `_IN_WORKTREE`，如果是则直接使用
3. **扫描已有 worktree**：`git worktree list` 查找最近的 `eng/*` 分支
4. **当前分支为 feature 分支**：如果当前在 `eng/*` 或非 `main` 分支，可以直接测试
5. **询问用户**：如果当前在 main 且无 worktree，通过 AskUserQuestion 询问

确认后输出：
```
🔧 测试环境：
  Worktree: /path/to/.worktrees/feature-slug (或 "当前目录")
  Branch:   eng/feature-slug-2026-03-28
  Base:     main
```

### 0.2 文档链定位

按搜索模式定位所有参考文档，forge-dev 传入的路径优先级最高：

```bash
# PRD
for f in docs/PRD.md PRD.md docs/*PRD*; do [ -f "$f" ] && echo "PRD: $f" && break; done

# DESIGN
for f in DESIGN.md docs/DESIGN.md docs/DESIGN-BLUEPRINT.md; do [ -f "$f" ] && echo "DESIGN: $f" && break; done

# ENGINEERING
for f in docs/ENGINEERING.md ENGINEERING.md; do [ -f "$f" ] && echo "ENGINEERING: $f" && break; done

# FEEDBACK（历史用户反馈，回归测试用）
for f in FEEDBACK.md docs/FEEDBACK.md; do [ -f "$f" ] && echo "FEEDBACK: $f" && break; done

# QA
for f in docs/QA.md QA.md; do [ -f "$f" ] && echo "QA: $f" && break; done

# .features/status
ls .features/*/status.md 2>/dev/null | head -5
```

**文档版本校验**：读取文档后提取版本号，与 `.features/status.md` 中记录的 PRD 版本对比。不一致则警告。

**降级模式**：如果找不到 PRD/DESIGN → 降级为"无文档模式"（只做 console + 响应式 + 可访问性基础测试）。

### 0.3 变更范围分析

```bash
# 基准分支
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo "main")

# 变更文件列表
git diff $BASE...HEAD --name-only 2>/dev/null
git diff $BASE...HEAD --stat 2>/dev/null

# 变更摘要
git log $BASE..HEAD --oneline 2>/dev/null
```

变更文件 → 推断影响范围 → 决定测试重点（Diff-aware 模式）。

### 0.4 选择器审计（铁律：不盲猜选择器）

**在生成 test-spec 前，必须扫描代码确认可用选择器。** 不同项目的 DOM 结构完全不同，不能假设任何 `data-testid` 或 ARIA 属性存在。

```bash
# 扫描项目中可用的选择器锚点
echo "=== data-testid ==="
grep -r 'data-testid' src/ --include='*.tsx' --include='*.jsx' --include='*.vue' --include='*.html' -l 2>/dev/null | head -10

echo "=== data-* 属性 ==="
grep -roh 'data-[a-z_-]*=' src/ --include='*.tsx' --include='*.jsx' --include='*.vue' -h 2>/dev/null | sort -u | head -20

echo "=== ARIA 属性 ==="
grep -roh 'role="[^"]*"\|aria-[a-z]*=' src/ --include='*.tsx' --include='*.jsx' --include='*.vue' -h 2>/dev/null | sort -u | head -20

echo "=== 语义化 HTML ==="
grep -roh '<\(nav\|main\|aside\|header\|footer\|section\|article\|dialog\)[> ]' src/ --include='*.tsx' --include='*.jsx' --include='*.vue' -h 2>/dev/null | sort | uniq -c | sort -rn | head -10
```

**根据扫描结果决定选择器策略：**

| 项目状态 | 选择器策略 | test-spec 中使用 |
|---------|-----------|----------------|
| 有丰富 `data-testid` | 直接使用 testid | `[data-testid='feed-section']` |
| 有 `data-*` 属性但非 testid | 使用已有 data 属性 | `[data-platform='twitter']`, `[data-item-id]` |
| 有 ARIA 属性 | 使用 role + aria | `[role='tab']`, `[aria-selected='true']` |
| 有语义化 HTML | 使用语义标签 | `main`, `nav`, `dialog` |
| 以上都没有 | **文本 + CSS 组合** | `button:has-text("搜索")`, `.card-container > .card:nth-child(1)` |

**选择器优先级（从稳定到脆弱）：**

```
1. getByRole('tab', { name: '推荐' })    ← 最稳定，语义化
2. [data-testid='feed-section']           ← 专为测试设计
3. [data-platform='twitter']              ← 业务语义属性
4. [role='dialog']                        ← ARIA 属性
5. button:has-text("搜索")                ← 可见文本
6. main > section:first-child             ← 结构选择器
7. .bg-card.rounded-lg                    ← CSS class（最脆弱）
```

**如果项目零 data-testid：**
- **不要在 test-spec 中编造 `data-testid`**——这会导致所有测试因选择器找不到而假性 FAIL
- 使用上述优先级中实际存在的选择器
- 在 QA 报告的"改进建议"中标注：建议 forge-eng 在关键交互元素上补充 `data-testid`

**输出选择器映射表**（供 test-spec 生成时引用）：

```
🔍 选择器审计结果：
  data-testid: 0 个（项目未使用 testid）
  data-* 属性: data-platform, data-item-id, data-section
  ARIA: role="dialog" (1处), role="button" (3处)
  语义标签: main, nav, section, header

  推荐策略：data-* 属性 + 文本选择器 + 语义标签组合

  关键元素映射：
  ├── 信息卡片: [data-platform] 或 .cursor-pointer:has(h3)
  ├── 详情面板: [role="dialog"] 或 [class*="detail"]
  ├── Tab 导航: button:has-text("推荐") 等
  └── 搜索框: input[type="search"] 或 input[placeholder*="搜索"]
```

### 0.5 测试级别与模式

**测试级别**（如用户未指定，通过 AskUserQuestion 确认）：

- A) **快速** — 只测 P0 核心流程（约5-10分钟）→ Phase 1+2
- B) **标准** — 快速 + P1 视觉/响应式（约15-30分钟）→ Phase 1+2+3
- C) **详尽** — 标准 + P2-P3 可访问性和边界（约30-60分钟）→ Phase 1+2+3+4

**测试模式**（自动选择）：

| 模式 | 触发条件 | 行为 |
|------|---------|------|
| **Diff-aware** | 在 feature 分支且有 base diff | 从 diff 推断影响范围，聚焦测试 |
| **Full** | 指定了 URL 或用户要求 | 系统性遍历所有页面 |
| **Regression** | 存在 FEEDBACK.md 或历史 QA 报告 | 优先测试历史反馈项 + 变更回归 |

---

## 第1步：建立健康基准

**在测试前打分（0-100分）：**

| 维度 | 权重 | 评估方式 |
|------|------|---------|
| 控制台错误 | 15% | JS 错误数量（0→100, 1-3→70, 4-10→40, 10+→10）|
| 链接完整性 | 10% | 死链数量（每个 -15，最低 0）|
| 核心功能 | 20% | 主要用户流程是否可用 |
| 视觉呈现 | 10% | 页面布局、样式是否正确 |
| 用户体验 | 15% | 交互流畅度、反馈及时性 |
| 性能 | 10% | 首屏加载、LCP、CLS |
| 内容 | 5% | 文案、数据展示是否正确 |
| 无障碍 | 15% | 键盘导航、对比度、语义化 |

使用 gstack/browse 或 Playwright 截取基准截图和控制台状态。

---

## 第2步：理解现状

### 迭代模式（已有 QA.md）
1. 读取 PRD 最新迭代摘要，提取验收标准
2. 读取 ENGINEERING.md，提取 API 契约和测试矩阵
3. 读取 DESIGN.md，提取视觉硬规则（字号、颜色、间距）
4. 读取 FEEDBACK.md（如有），提取历史用户反馈 → 纳入回归基线
5. 读取 QA.md + QA CHANGELOG，做热点分析
6. 向用户总结当前状态

### 从零创建模式（无 QA.md）
1. 分析项目测试现状（检查 tests/、覆盖率、CI 配置）
2. 与用户多轮确认（测试策略、范围、验收标准）
3. 产出 QA.md 初稿（参考 [references/qa-template.md](references/qa-template.md)）

---

## 第2.5步：生成 test-spec（铁律：不生成就不执行）

### 输入 → 输出映射

| 输入源 | 提取内容 | 转化为 |
|--------|---------|-------|
| PRD 验收标准 | "用户点击卡片，弹出详情面板" | `functional` 断言 |
| DESIGN.md 规则 | "最小字号 12px"、"4px 间距网格" | `visual` CSS 断言 |
| ENGINEERING.md API | "GET /api/feed → { items: [...] }" | `network` 断言 |
| git diff | "修改了 DetailPanel.tsx" | `regression` 聚焦断言 |
| 会话上下文 | "刚实现了频道切换功能" | `functional` 断言 |
| FEEDBACK.md | 历史用户反馈 | `regression` 回归断言 |

### test-spec.json 结构

**重要：选择器必须来自 Step 0.4 的审计结果，不能编造不存在的 `data-testid`。** 下面的示例用 `$SELECTOR_*` 占位符表示"根据审计结果填充实际选择器"。

```json
{
  "metadata": {
    "source": "PRD.md v10.1 + DESIGN.md v2",
    "branch": "eng/feature-slug-2026-03-28",
    "generated_at": "2026-03-28T10:00:00Z",
    "scope": "full | diff-aware | regression",
    "app_url": "http://localhost:8080",
    "selector_strategy": "data-* + text + semantic"
  },
  "selector_map": {
    "_comment": "Step 0.4 审计产出，所有 case 引用此映射",
    "feed_section": "main > section:first-child",
    "info_card": ".cursor-pointer:has(h3)",
    "detail_panel": "[role='dialog']",
    "tab_nav": "nav button",
    "search_input": "input[placeholder*='搜索']"
  },
  "suites": [
    {
      "id": "feed-display",
      "name": "信息流展示",
      "source_ref": "PRD.md#v10.0-信息流",
      "priority": "P0",
      "cases": [
        {
          "id": "feed-001",
          "description": "首页加载后展示信息卡片",
          "dimension": "functional",
          "steps": [
            { "action": "navigate", "url": "/" },
            { "action": "wait", "selector": "main > section:first-child", "timeout": 8000 }
          ],
          "assertions": [
            { "type": "visible", "selector": "main > section:first-child" },
            { "type": "count_gte", "selector": ".cursor-pointer:has(h3)", "min": 1 },
            { "type": "contains_text", "selector": "main", "texts": ["$EXPECTED_SECTION_TITLE"] },
            { "type": "no_console_errors" }
          ]
        },
        {
          "id": "feed-002",
          "description": "卡片点击→详情面板，验证内容完整性（数据驱动）",
          "dimension": "data-driven",
          "data_driven": {
            "selector": ".cursor-pointer:has(h3)",
            "sample_size": 15,
            "strategy": "stratified"
          },
          "steps": [
            { "action": "click", "selector": "$item" },
            { "action": "wait", "selector": "[role='dialog']", "timeout": 5000 }
          ],
          "assertions": [
            { "type": "visible", "selector": "[role='dialog']" },
            { "type": "evaluate", "description": "详情面板有标题且文本长度 > 0",
              "script": "const panel = document.querySelector('[role=\"dialog\"]'); const title = panel?.querySelector('h2, h3'); if (!title || title.textContent.trim().length === 0) throw new Error('详情面板标题为空')" },
            { "type": "evaluate", "description": "详情面板有实质内容（不只是骨架屏）",
              "script": "const panel = document.querySelector('[role=\"dialog\"]'); const textLen = panel?.innerText?.trim().length || 0; if (textLen < 50) throw new Error(`面板内容过短: ${textLen} 字符`)" },
            { "type": "no_console_errors" }
          ]
        }
      ]
    }
  ]
}
```

**选择器规则**（参考 Step 0.4 审计 + Playwright 最佳实践）：
- 只使用审计中确认存在的选择器，**绝不编造不存在的 `data-testid`**
- 优先级：`role/aria` > `data-*` 属性 > 语义标签 > 可见文本 > CSS class 组合
- 如果项目缺乏稳定选择器，在 QA 报告的"改进建议"中提出，交由 forge-eng 补充

### 断言深度规则（铁律 4 的展开）

**核心原则："it renders" ≠ "it works correctly"。**

每个 test case 的 assertions 数组必须包含至少一个**深层断言**。`visible` 和 `count_gte` 只能作为前置条件（确认元素在 DOM 中），不能作为验收断言。

#### ❌ 反面示例（浅断言 — 只验证"存在"不验证"正确"）

```json
{
  "id": "starred-001",
  "description": "收藏页展示收藏的卡片",
  "assertions": [
    { "type": "visible", "selector": "section.starred-view" },
    { "type": "count_gte", "selector": ".cursor-pointer:has(h3)", "min": 1 }
  ]
}
```
问题：只验证了"收藏页有卡片"，没验证卡片**确实是收藏的**、内容**确实渲染了**。

#### ✅ 正面示例（深层断言 — 验证数据正确性和功能完整性）

```json
{
  "id": "starred-001",
  "description": "收藏页展示收藏的卡片",
  "assertions": [
    { "type": "visible", "selector": "section.starred-view" },
    { "type": "count_gte", "selector": ".cursor-pointer:has(h3)", "min": 1 },
    { "type": "evaluate", "description": "每张卡片有标题且标题非空",
      "script": "const cards = document.querySelectorAll('.cursor-pointer:has(h3)'); cards.forEach((c, i) => { const title = c.querySelector('h3'); if (!title || title.textContent.trim().length === 0) throw new Error(`第 ${i+1} 张卡片标题为空`) })" },
    { "type": "evaluate", "description": "收藏页卡片数量与页面显示的统计数一致",
      "script": "const displayed = document.querySelectorAll('.cursor-pointer:has(h3)').length; const header = document.querySelector('h2, [class*=\"header\"]')?.textContent || ''; const match = header.match(/(\\d+)/); if (match && displayed !== parseInt(match[1])) throw new Error(`显示 ${displayed} 张但标题显示 ${match[1]}`)" }
  ]
}
```

#### 更多断言深度检查表（生成 test-spec 时逐条对照）

| 测试场景 | 浅断言（❌ 不够） | 深层断言（✅ 必须） |
|---------|-----------------|-------------------|
| 详情/弹窗 | `panel.isVisible()` | `panel.innerText.length > 50` + 包含标题/关键区块 |
| 列表/收藏 | `cards.count() > 0` | 每张卡片有标题且非空，数量与页头统计一致 |
| Tab/频道切换 | `section.isVisible()` | 切换后内容区文本变化（不是切前的旧内容） |
| SSE/流式生成 | `button.isVisible()` | 触发 → 中间态可观测 → 完成后结果持久化（reload 仍在） |
| 搜索/过滤 | `results.isVisible()` | 结果包含关键词，数量合理，空结果有空状态提示 |
| 模态框/对话框 | `dialog.isVisible()` | 有标题 + 正文文本长度 > 0 + Escape 可关闭 |
| 表单提交 | `form.isVisible()` | 填充 → 提交 → 反馈出现（toast/跳转/数据变化） |
| URL/深度链接 | `page.loaded()` | 直接访问带参数的 URL → 视图状态与参数一致 |
| 懒加载内容 | `skeleton.gone()` | 等待加载完成 → 内容非空 → 数量/值与预期一致 |

#### 自检规则

生成 test-spec 后，**自动扫描**所有 case：
- 如果某个 case 的 assertions 只有 `visible`/`count_gte`/`hidden` 类型 → **标记为 ⚠️ 浅断言**，必须补充深层断言
- 如果某个 case 没有任何 `contains_text`/`evaluate`/`has_attribute`/`css_value`/`matches_regex` → **拒绝执行**，回到 test-spec 生成步骤补充

### test-spec 不是手写的

test-spec 由 Claude 基于文档理解自动生成，但它是**结构化的、可审查的**。生成后必须输出摘要供用户确认。

---

## 第3步：生成验收计划并请用户确认

**铁律：不是技术 test-spec 的摘要，而是用户可读的验收计划。** 用户需要先理解"要验什么"，才能判断测试是否充分。

### 3.1 检查 Feature Spec

读取 PRD 中的 Feature Spec 章节。如果存在：
- 从 Feature Spec 的验收检查表提取所有验收项
- 将 Given/When/Then 场景映射为 test-spec 用例
- Feature Spec 的验收检查表是 QA 的**主要输入**，test-spec 的每个用例 SHALL 可追溯到 Feature Spec 中的某个场景

如果 Feature Spec 不存在：
- 通过 AskUserQuestion 警告：「PRD 中没有 Feature Spec，QA 将基于 PRD 功能描述生成测试，但验收标准可能不够精确。建议先运行 /forge-prd 补充 Feature Spec。」
- 如果用户选择继续，降级为从 PRD 功能描述 + DESIGN.md 提取验收项

### 3.2 生成验收计划文档

基于 Feature Spec（或降级来源），生成一份**先全局后细节**的验收计划：

```markdown
## QA 验收计划：{功能名}

### 全局验证（先看整体是否符合预期）

#### 用户流程完整性（对标 Feature Spec 第一节）
- [ ] 用户流程从 {入口} 到 {出口} 无断点
- [ ] 异常路径均有对应的错误处理
- [ ] 流程图中的每个步骤在实际页面中都可达

#### 页面/系统结构合规性（对标 Feature Spec 第二节）
- [ ] 整体布局与 Feature Spec 的结构图一致
- [ ] 各区块职责与描述匹配
- [ ] 组件列表完整，无遗漏无多余

---

### 逐项验证（再看具体细节）

| # | 验收项 | 来源 | 测试方法 | 断言类型 |
|---|--------|------|---------|---------|
| 1 | {场景描述} | Feature Spec: {Requirement名}.正常 | {Playwright/gstack} | {contains_text/css_value/...} |
| 2 | {场景描述} | Feature Spec: {Requirement名}.异常 | ... | ... |
| ... | ... | ... | ... | ... |

---

### 视觉合规验证（对标 DESIGN.md + Feature Spec CSS 约束）

| # | 组件 | CSS 属性 | 预期值 | 断言方式 |
|---|------|---------|--------|---------|
| V1 | {组件名} | font-size | {值} | css_value |
| V2 | {组件名} | color | {值} | css_value |
| V3 | {组件名} | padding | {值} | css_value |
| ... | ... | ... | ... | ... |

如果存在 Image 2 视觉稿、`.do-dev/visual-decision.md` 或 `.deliver/visual-decision.md`，在计划中单列「视觉意图参考」：说明会用真实浏览器截图对比信息层级、密度、主操作和空态/错态覆盖。Image 2 不作为 pass/fail 证据，pass/fail 只来自 Feature Spec、DESIGN.md、CSS 属性、行为断言和真实截图。

---

共 {N} 项验收（功能 {X} 项 + 视觉 {Y} 项 + 流程 {Z} 项），预计 {时间}。
```

### 3.3 用户确认

通过 AskUserQuestion 展示验收计划摘要并等待确认：

```
📋 验收计划已生成（基于 Feature Spec + DESIGN.md）

全局验证：
  - 用户流程完整性：{步骤数} 步
  - 页面结构合规性：{区块数} 区块，{组件数} 组件

逐项验证：
  - 功能场景：{X} 项（{功能点数} 个功能点 × 3 场景）
  - 视觉合规：{Y} 项（CSS 属性断言）
  - 流程完整：{Z} 项

A) 确认执行
B) 需要增减测试项（说明哪些）
C) 需要看完整验收计划再决定
D) Feature Spec 有误，需要先修正
```

**⚠️ 用户确认后才执行测试。**

---

## 第4步：更新 QA 文档

1. 更新/创建 QA.md（参考 [references/qa-template.md](references/qa-template.md)）
2. 更新 QA CHANGELOG
3. 将 test-spec.json 保存到报告目录

---

## 第5步：7 维度测试执行

**使用 qa-runner.mjs 框架。** 详细代码模板参考 [references/test-dimensions.md](references/test-dimensions.md)。

### 测试脚本编写规范

**必须使用 qa-runner.mjs 框架**，不从零写脚本：

```javascript
import { TestCollector, attachMonitors, snap, snapElement, createPage, pickStratified, writeResults } from '$QA_RUNNER';

const collector = new TestCollector();
const { browser, page } = await createPage();
attachMonitors(page, collector);

// ... 测试逻辑（使用 collector.pass/fail/skip）...

collector.printSummary();
writeResults(collector);
await browser.close();
process.exit(collector.summary().failed > 0 ? 1 : 0);
```

**`$QA_RUNNER` 替换为前置脚本检测到的路径。**

### 执行分阶段（快速失败）

```
Phase 1 冒烟（所有级别都执行）
  ├── 控制台零容忍 [console]：page.on('pageerror') + page.on('console error')
  ├── 首页加载：导航 → 等待 → 断言核心元素可见
  └── API/网络基础 [network]：检查 /api/* 状态码 < 400
  → 如果 Phase 1 全 FAIL → 停止测试（环境问题），报告并退出

Phase 2 核心功能（快速+标准+详尽）
  ├── 交互完整性 [functional]：Tab 切换、按钮点击、模态框开关
  ├── 数据驱动遍历 [data-driven]：采样 N 个元素，逐一验证
  ├── SSE/流式生成 [streaming]：全链路（触发→中间态→完成→持久化），有 SSE 时启用
  ├── URL 状态 [url-state]：正反向验证（操作→URL + URL→视图恢复），有路由状态时启用
  └── 懒加载/异步 [async-content]：加载态→内容验证→分页/进度，有异步加载时启用
  → 覆盖 P0 用例

Phase 3 视觉+响应式（标准+详尽）
  ├── 视觉规则断言 [visual]：CSS 属性验证（字号、颜色、间距）
  └── 响应式断点 [responsive]：375/768/1440 三个视口
  → 覆盖 P1 用例

Phase 4 深度（仅详尽级别）
  ├── 可访问性 [accessibility]：axe-core WCAG 2.0 AA
  └── 边界条件：空数据、超长文本、网络异常
  → 覆盖 P2-P3 用例
```

### 7 维度概述

#### 维度 1: 控制台零容忍 [console]

`attachMonitors()` 自动挂载。每个导航/交互后通过 `collector.checkConsoleErrors()` 检查。
任何 `pageerror` = 自动 FAIL，包含错误文本和 stack trace。

**能发现**：React 渲染崩溃、未捕获异常、404 资源
**不能发现**：被 try-catch 包裹的静默错误

#### 维度 2: 数据驱动遍历 [data-driven]

不测 1 个元素，采样 N 个。使用 `pickStratified()` 分层采样（首尾 + 均匀分布）。
每个元素独立 pass/fail，统计崩溃率并推算总体影响。

**能发现**：27% 卡片因数据类型不一致崩溃（当前完全测不到的）
**不能发现**：需要特定数据组合才触发的 bug

#### 维度 3: 网络契约验证 [network]

`attachMonitors()` 自动收集 `/api/*` 响应。断言：状态码 < 400 + 响应结构匹配。
如果 ENGINEERING.md 定义了 API schema，验证响应 JSON 结构。

**能发现**：API 404、响应结构变更、后端未启动
**不能发现**：语义正确但数据错误的响应

#### 维度 4: 视觉规则断言 [visual]

从 DESIGN.md 提取硬规则 → CSS 断言。使用 `page.evaluate(el => getComputedStyle(el))`。
检查项：字号 ≥ 12px、间距遵循 4px 网格、平台配色正确。
如果有 Image 2 视觉稿，仅用作解释偏差的参考，不能替代 CSS 断言或真实截图。

**能发现**：字号不达标、间距违规、颜色错误
**不能发现**："看起来不对但 CSS 值合规"的美学问题

#### 维度 5: 交互完整性 [functional]

每个可交互元素：操作 → 状态变化断言 → 可逆性验证。
Tab: `click → aria-selected === true → panel visible`
模态框: `click → modal visible → Escape → modal gone`

**能发现**：Tab 崩溃、按钮无响应、模态框不可关闭
**不能发现**：交互流畅度、动画是否自然

#### 维度 6: 响应式断点 [responsive]

三个断点：mobile(375×812) / tablet(768×1024) / desktop(1440×900)。
每个断点检查：无水平溢出 + 触控目标 ≥ 44px + 截图留证。

#### 维度 7: 可访问性 [accessibility]

axe-core WCAG 2.0 AA 扫描 + 键盘导航验证（Tab 遍历 + Enter 激活 + Escape 关闭）。

#### 维度 8: SSE / 流式生成全链路 [streaming]

**适用条件**：项目包含 SSE 端点、WebSocket、流式 AI 生成等实时特性。通过 Step 0.4 扫描 `EventSource`、`fetch.*stream`、`WebSocket` 判断是否启用。

测试全生命周期，不只是"按钮存在"：

```
触发入口（按钮/表单）→ 中间态（loading/thinking/progress）→ 数据流（逐步到达）→ 完成态 → 持久化验证（reload 后数据仍在）
```

关键断言：
- 触发后：中间态 UI 出现（spinner/进度条/thinking 动画），按钮变为不可操作
- 流式期间：内容区逐步增长（`textContent.length` 单调递增）
- 完成后：loading 消失，最终内容完整渲染
- 取消/中断：如果有取消按钮，点击后回到 idle 态，无残留
- **持久化**：刷新页面后，生成的内容仍然存在（最关键的深层断言）
- 错误恢复：模拟网络中断（`page.route` 拦截 → abort），UI 显示错误态而非卡死

#### 维度 9: URL 状态 / 深度链接 [url-state]

**适用条件**：项目使用 hash 路由（`#view=xxx`）、query 参数（`?tab=xxx`）、或 SPA 路由（`/page/xxx`）管理视图状态。通过 Step 0.4 扫描 `useHash`、`useRouter`、`history.pushState`、`window.location.hash` 判断是否启用。

测试双向一致性：

```
操作 → URL 变化        （正向：UI 操作驱动 URL 更新）
URL → 视图恢复         （反向：直接访问 URL 恢复完整状态）
```

关键断言：
- **正向**：点击 Tab/频道/卡片 → `page.url()` 包含对应参数
- **反向**：直接 `page.goto(url_with_params)` → 视图状态正确恢复（Tab 选中、内容加载）
- **深度链接**：带完整参数的 URL（如 `#l1=recommend&d=item-123`）→ 详情面板自动打开，内容正确
- **浏览器前进/后退**：`page.goBack()` / `page.goForward()` → 视图正确切换
- **边界**：无效参数的 URL（如 `#d=nonexistent-id`）→ 优雅降级，不白屏

#### 维度 10: 懒加载 / 异步内容 [async-content]

**适用条件**：项目包含分页加载、无限滚动、骨架屏、点击后异步获取详情等模式。几乎所有现代 SPA 都适用。

测试加载全生命周期：

```
触发加载 → 加载态（skeleton/spinner）→ 内容到达 → 加载态消失 → 内容正确
```

关键断言：
- **等待策略**：不用 `waitForTimeout` 硬等，使用 `waitForResponse` 或 `waitForSelector` 等具体条件
- **骨架屏消失**：如果有 skeleton，等待 `.skeleton` 消失再断言内容
- **内容非空**：加载完成后，内容区 `textContent.length > 0`（不只是 skeleton 被替换为空 div）
- **分页/进度**：如果有进度提示（"加载中 500/10740"），验证进度文本格式正确，全部加载完成后进度消失
- **滚动加载**：`page.mouse.wheel` 或 `scrollIntoView` 触发加载 → 新内容出现 → 总量增加
- **加载失败**：`page.route` 拦截 API 返回 500 → 显示错误提示而非无限 loading

**通用等待模式**（替代 `waitForTimeout`）：

```javascript
// ❌ 硬等（不可靠，慢）
await page.waitForTimeout(3000);

// ✅ 等 API 响应（精确）
await page.waitForResponse(resp => resp.url().includes('/api/feed') && resp.status() === 200);

// ✅ 等骨架屏消失（语义化）
await page.waitForSelector('.skeleton', { state: 'hidden', timeout: 10000 });

// ✅ 等内容出现（直接）
await page.waitForSelector('main .cursor-pointer:has(h3)', { timeout: 10000 });

// ✅ 等网络空闲（兜底）
await page.waitForLoadState('networkidle');
```

### Codex Browser Use 引擎（用户视角浏览器验收）

在 Codex 环境中，如果 Browser Use 插件可用，前端页面、交互、视觉、控制台检查优先使用 `browser-use:browser`：

- 使用 Codex in-app browser 打开调用方传入的 `APP_URL`
- 通过 DOM snapshot 构造稳定 locator，不盲猜选择器
- 每次点击、输入、切换、提交后采集最小必要状态：DOM snapshot 或截图
- 关键状态节点截图保存到 QA 报告或 Bug 修复验收报告指定目录，并用 Markdown 内嵌
- 读取 console logs，任何 error 进入 FAIL
- 如果代码或 build 刚变更，测试本地页面前先 reload，再重新采集 DOM/screenshot

使用规则：

1. 执行浏览器动作前必须先加载并遵守 `browser-use:browser` skill。
2. 初始化 Browser 时使用 `iab` backend 和 Node REPL 的 browser-client runtime。
3. 不用 Computer Use 操作浏览器，除非 Browser Use 不可用、被中断或目标不是浏览器页面；兜底原因必须写入报告。
4. Browser Use 负责用户视角证据，仍需配合断言。截图不能单独作为 PASS。
5. 需要可重复批量回归时，Browser Use 证据可与 Playwright 脚本断言并行使用。

### gstack/browse 引擎（快速探索和截图标注）

当 gstack/browse 可用时，可作为 Playwright 的补充：

```bash
$B goto <URL>
$B snapshot -i -a     # 标注所有可交互元素
$B console --errors   # 控制台错误
$B network            # 网络请求
$B perf               # LCP、CLS 性能
$B screenshot $REPORT_DIR/screenshots/overview.png
$B responsive         # 三视口截图
```

**引擎协同**：
- browser-use:browser：Codex in-app browser 用户视角操作、DOM 快照、截图证据
- Playwright + qa-runner：结构化断言、数据驱动、网络拦截、可重复回归
- gstack/browse：快速探索、截图标注、性能指标

### 纯代码测试（无浏览器引擎时）

- 逐文件读取实现代码，检查边界情况
- 验证错误处理完整性
- 检查 API 输入验证
- 运行项目已有的测试框架（`npm test` / `pytest` 等）

---

## 第6步：智能分析 + Bug 报告

### 分析流程

对每个 FAIL 的测试用例：

1. **错误分类**
   - Console Error → 提取 stack trace → 定位源文件:行号
   - 元素不存在 → 检查选择器 → 检查组件是否渲染
   - 网络错误 → 检查后端日志 → 检查 API 路由
   - 视觉偏差 → 检查 CSS 来源 → 对比 DESIGN.md 规则

2. **交叉验证**
   - 将 console error 中的文件路径 → 对应到 git diff 中的变更文件
   - 在 diff 中 → 标记 `[本次引入]`
   - 不在 diff 中 → 标记 `[已有问题]`

3. **影响范围估算**
   - data-driven 测试：5/20 崩溃 → 推算 25% 数据受影响
   - 功能测试：特定 tab 崩溃 → 标记该 tab 下所有功能受影响

### Bug 登记格式

```markdown
### BUG-001 [严重度] 标题

**现象：** 用户看到了什么
**影响：** 影响范围（如"25% 的卡片无法打开详情"）
**证据：**
  - Console: "错误信息原文"
  - Stack: `文件:行号`
  - 截图: qa_screenshots/XX_name.png
**根因定位：**
  - 文件: `src/components/DetailPanel.tsx:360`
  - 原因: 一句话说明
**本次引入：** 是/否（基于 git diff 交叉引用）
**修复建议：** 简要描述修复思路
```

### 严重度分类

| 严重度 | 定义 | 处理 |
|--------|------|------|
| 严重 | 核心功能崩溃/不可用 | 必须修复 |
| 高 | 功能可用但结果错误 | 必须修复 |
| 中 | 功能可用但体验差 | 建议修复 |
| 低 | 外观/措辞/细节问题 | 可延后 |

### 修复清单产出

```markdown
# 修复清单（forge-qa 生成）

## 必须修复（严重 + 高）
- [ ] BUG-001: {现象} — {文件:行号} — {修复方向}
- [ ] BUG-002: ...

## 建议修复（中）
- [ ] BUG-003: ...

## 可延后（低）
- [ ] BUG-005: ...
```

### QA 自动闭环交付给 forge-bugfix

当 forge-qa 处于功能开发后的自动闭环场景，发现属于本轮 Feature Spec 或本次 diff 引入的 bug 时，不能只写散文报告，必须为 forge-bugfix 准备结构化输入：

```markdown
### BF-CANDIDATE: {标题}

**建议严重度**：P0 / P1 / P2
**是否属于本轮范围**：是 / 否 / 待用户判断
**关联 Feature Spec**：docs/PRD.md#...
**用户可见现象**：...
**复现步骤**：
1. ...
2. ...
3. ...
**前端地址**：...
**后端/API 地址**：...
**环境身份摘要**：Frontend PID/cwd, Backend PID/cwd, commit
**截图证据**：
![](./qa_screenshots/BUG-001-step-01.png)
![](./qa_screenshots/BUG-001-step-02.png)
**深度断言失败**：文本/状态/URL/CSS/网络/数据断言摘要
**console/network 证据**：...
**建议交给 forge-bugfix 的原因**：...
```

调度层或 forge-dev 可以把这些候选写入 `docs/bugfix/backlog.md`，创建对应 `docs/bugfix/reviews/BF-XX.md`，然后逐个调用 forge-bugfix。forge-qa 自己仍然只测不修。

自动闭环分类规则：

| 分类 | 处理 |
|---|---|
| 属于本轮 Feature Spec / 本次 diff 引入 | 自动登记 BF，进入 forge-bugfix |
| 回归破坏核心流程 | 自动登记 BF，进入 forge-bugfix |
| 新需求或设计取舍 | 登记为待用户判断，不自动修 |
| 范围外低优先级问题 | 登记 backlog，不阻塞本轮 |
| 环境身份无法确认 | BLOCKED_HUMAN，不进入 bugfix |

---

## 第7步：User Gate（用户验收关卡）

**铁律：不可跳过。** QA 自动化测试无法覆盖设计意图偏差、功能遗漏等只有用户能判断的问题。

### 输出与等待

QA 报告生成后，输出以下内容并等待用户操作：

```
╔══════════════════════════════════════════╗
║           QA 报告已生成                   ║
╠══════════════════════════════════════════╣
║  通过: 10  失败: 3  跳过: 1              ║
║  健康评分: 72/100                        ║
║  报告: .gstack/qa-reports/qa-report-*.md ║
╠══════════════════════════════════════════╣
║  请验收后选择：                            ║
║  A) 验收通过 → 进入发布流程                ║
║  B) 验收不通过 → 填写反馈，回 forge-eng     ║
║  C) 我需要先自己体验一下                    ║
╚══════════════════════════════════════════╝
```

### 用户操作

**A) 验收通过（accept）**
- 更新 `.features/status.md` qa 行为 `[✅ 已完成]`
- 建议下一步：`/forge-review` 或 `/forge-ship`

**B) 验收不通过（reject）**
- 引导用户描述问题（可以直接在会话中描述）
- Claude 自动提取为 FEEDBACK.md 格式
- **⚠️ 触发举一反三机制（见下方）**
- 合并 qa-report 中未修复的 BUG + 用户 FEEDBACK + 举一反三发现
- 生成统一修复清单 → `/forge-eng`

### 举一反三机制（用户反馈问题时 SHALL 执行）

当用户报告任何问题时，SHALL 按以下步骤执行：

1. **修复用户指出的问题**

2. **搜索相似模式**：
   - 使用 Grep 在代码库中搜索与该问题相同的模式（同类 CSS 属性、同类组件、同类逻辑）
   - 示例：用户报告「某组件间距不对」→ Grep 搜索所有使用相同 margin/padding 值的组件

3. **回查 Feature Spec**：
   - 读取 PRD 中的 Feature Spec，检查其他行为场景是否可能存在同类问题
   - 检查验收检查表中未测试的项是否包含类似约束

4. **产出「类似风险清单」**：
   ```markdown
   ### 举一反三：类似风险清单
   
   用户反馈：{用户描述的问题}
   根因：{问题的根本原因}
   
   发现 {N} 处类似风险：
   1. {文件路径:行号} — {组件/模块名} 使用了相同的 {模式}，可能存在同样问题
   2. {文件路径:行号} — Feature Spec 场景 {场景名} 的 THEN 要求 {约束}，当前实现为 {实际值}
   3. ...
   ```

5. **请用户确认**：
   ```
   发现 {N} 处类似风险，要一并修复吗？
   A) 全部修复
   B) 选择性修复（指定哪些）
   C) 只修复用户指出的问题，其余记录到 FEEDBACK.md
   ```

**SHALL NOT 仅修复用户明确指出的单点问题就声称完成。**

**C) 用户自行体验**
- 暂停，等待用户回来反馈
- 用户可以随时在会话中描述问题

### FEEDBACK.md 结构

```markdown
# User Feedback — {feature-name}

## 元数据
- 日期: YYYY-MM-DD
- QA 报告参考: qa-report-YYYY-MM-DD.md
- 分支: eng/feature-name-YYYY-MM-DD

## 反馈项

### UF-001 [Design Intent] 标题
**期望：** 用户期望的行为
**现状：** 实际看到的行为
**参考：** DESIGN.md#section 或 PRD.md#version
**截图：** feedback_screenshots/001.png（可选）

### UF-002 [Missing] 标题
**期望：** PRD 中描述的功能
**现状：** 功能缺失或未实现
**参考：** PRD.md#section
```

**反馈类型：**
- `[Design Intent]` — 设计意图偏差（QA 测不到的，只有用户能判断）
- `[Missing]` — 功能缺失（PRD 有但没实现）
- `[Regression]` — 回归问题（之前好的现在坏了）
- `[Polish]` — 打磨细节（能用但不够好）

### FEEDBACK.md 的流转

| 谁 | 怎么用 |
|----|-------|
| **forge-eng** | 读取 → 作为 fix list，和 qa-report BUG 一起修 |
| **forge-qa（下一轮）** | 读取 → 纳入 test-spec 回归项，确保不再漏测 |
| **forge-qa（长期）** | 历史 FEEDBACK 累积为项目回归测试基线 |
| **用户** | 只写"发现了什么 + 期望什么"，不需要定位根因 |

### 反馈闭环流程

```
QA 报告 → User Gate → reject
                        │
                        ↓
                  FEEDBACK.md（用户反馈）
                        │
                        ↓
                  合并修复清单 = qa-report BUG + FEEDBACK
                        │
                        ↓
                  forge-eng（修复）
                        │
                        ↓
                  forge-qa（回归）
                    ├── test-spec 自动包含 FEEDBACK 项
                    └── 只测变更 + FEEDBACK 涉及范围
                        │
                        ↓
                  User Gate（再次验收）
                        │
                        └── ... 直到 accept
```

---

## 第8步：健康评分与报告

### 健康评分计算

| 维度 | 权重 | 评分方式 |
|------|------|---------|
| 控制台错误 | 15% | 0 错误→100, 1-3→70, 4-10→40, 10+→10 |
| 链接完整性 | 10% | 每个死链 -15，最低 0 |
| 核心功能 | 20% | 每个严重 -25, 高 -15, 中 -8, 低 -3 |
| 视觉呈现 | 10% | 同上 |
| 用户体验 | 15% | 同上 |
| 性能 | 10% | 同上 |
| 内容 | 5% | 同上 |
| 无障碍 | 15% | 同上 |

### 报告结构（先全局后细节）

QA 报告 SHALL 采用以下结构，让用户先看整体是否符合预期，再审阅细节：

```markdown
# QA 验收报告：{功能名}

**日期**: YYYY-MM-DD  **分支**: {branch}  **PRD 版本**: vX.Y

---

## 一、全局评估（先看整体）

### 用户流程完整性
- 状态：PASS / FAIL
- 说明：{流程是否通畅，哪些步骤有问题}
- 证据：{流程截图或描述}

### 页面/系统结构合规性
- 状态：PASS / FAIL
- 说明：{整体布局是否符合 Feature Spec 第二节的结构图}
- 偏差项：{列出与 Feature Spec 不一致的区块/组件}

### 整体健康评分：XX/100

---

## 二、逐项验收结果（再看细节）

| # | 验收项 | 来源场景 | 结果 | 证据 |
|---|--------|---------|------|------|
| 1 | {描述} | {Feature Spec 场景} | ✅ PASS | {截图/日志} |
| 2 | {描述} | {Feature Spec 场景} | ❌ FAIL | {错误详情} |
| ... | ... | ... | ... | ... |

通过率：{X}/{Y} ({Z}%)

---

## 三、视觉合规结果

| # | 组件 | CSS 属性 | 预期值 | 实际值 | 结果 |
|---|------|---------|--------|--------|------|
| V1 | {名} | font-size | 14px | 14px | ✅ |
| V2 | {名} | color | #1e293b | #333 | ❌ |
| ... | ... | ... | ... | ... | ... |

---

## 四、发现的问题（按严重度排序）

{BUG 登记，格式同第6步}

---

## 五、验收结论

- 上线就绪：✅ / ⚠️ / ❌
- 必须修复：{N} 项
- 建议修复：{N} 项
- 举一反三风险：{N} 项（如有用户反馈触发）
```

### 报告输出

**输出到项目目录**：`$REPORT_DIR/qa-report-{YYYY-MM-DD}.md`

```
.gstack/qa-reports/
├── qa-report-{YYYY-MM-DD}.md      # 结构化报告（先全局后细节）
├── test-results.json               # 结构化结果（机器可读，qa-runner 产出）
├── test-spec.json                  # 测试规格（用于回归）
├── screenshots/                    # 截图证据
└── baseline.json                   # 回归基准数据
```

### 终端报告

```
+============================================================+
|                     QA 交付完成                              |
+============================================================+
| 项目：[项目名]      分支：[分支名]                            |
| 测试级别：快速 / 标准 / 详尽                                   |
| 测试引擎：qa-runner + gstack/browse                          |
+------------------------------------------------------------+
| 测试结果                                                     |
|   总计: XX  通过: XX  失败: XX  跳过: XX                      |
|   通过率: XX%  控制台错误: XX  网络错误: XX                    |
+------------------------------------------------------------+
| 健康评分：XX/100                                             |
| 上线就绪：✅ 可以上线 / ⚠️ 需关注 / ❌ 不建议                  |
+------------------------------------------------------------+
| 等待用户验收（User Gate）...                                   |
+============================================================+
```

---

## Feature 状态管理

### 启动时
- 读取 `.features/{feature-id}/status.md`，确认 eng 行为 `[✅ 已完成]`
- 将 qa 行更新为 `[🔄 进行中]`

### 执行中
- 更新 QA Items 表，每个测试项独立状态

### 完成时
- 通过：qa 行 `[✅ 已完成]`，note: `{passed}/{total} PASS, {score}/100`
- 未通过：qa 行 `[❌ 失败]`，note: `{failed} FAIL, 需修复后重测`
- 更新 `_registry.md` heartbeat

---

## 重要规则

1. **像真实用户一样测试** — 点所有可点的，填所有表单，测试所有状态。
2. **截图留证** — 每个测试步骤至少一张截图。用 `snapElement()` 紧凑裁剪，不用 fullPage。截图后用 Read 工具展示给用户。
3. **不要只测 Happy Path** — 边界、空状态、超长输入、网络错误都要测。
4. **控制台是第一现场** — 每次交互后检查控制台。视觉上没问题不代表没有 JS 错误。
5. **数据驱动是核心** — 不只测一条数据。用 `pickStratified()` 采样多条。
6. **前后端联动是重点** — 验证 API 调用是否正确、响应是否合理。
7. **深度优于广度** — 5-10 个证据充分的 Bug > 20 个模糊描述。
8. **自我调节** — 拿不准就停下来问。
9. **绝不拒绝使用浏览器** — 后端变更也会影响应用行为，始终打开浏览器测试。
10. **User Gate 不可跳过** — 自动化测不到设计意图偏差，必须等用户验收。

---

## 资源

- **QA 文档模板**：[references/qa-template.md](references/qa-template.md)
- **10 维度代码模板**：[references/test-dimensions.md](references/test-dimensions.md)
- **通用测试引擎**：[scripts/qa-runner.mjs](scripts/qa-runner.mjs)

---

## Mode B：单 bug 修复验收报告模式

> 🎯 配合 forge-bugfix 的 P6 调用。目标：针对**一个 bug 的 Bug 修复验收报告**跑自动化测试，把环境身份、逐步截图、深度断言回填到同一份报告里。

### B.1 前提与入口

- **触发方**：forge-bugfix 的 P6 节点
- **传入参数**：`REVIEW_DOC`（Bug 修复验收报告路径，形如 `docs/bugfix/reviews/BF-0419-2.md`）
- **跳过的节点**：不做 test-spec 生成（Layer 1）、不做 User Gate（那一步由 forge-bugfix 的 P6.5 做）
- **继承的能力**：仍然用 10 维度断言引擎和三种测试引擎（gstack / Playwright / 纯代码）

### B.2 读取 Bug 修复验收报告

```bash
# 必须存在
[ -f "$REVIEW_DOC" ] || { echo "❌ Bug 修复验收报告不存在: $REVIEW_DOC"; exit 1; }

# 读取报告全部内容
cat "$REVIEW_DOC"
```

AI 解析出：
- BUG_ID
- 修复 commit hash（用于定位修复范围）
- 涉及文件列表（用于缩小测试范围）
- TDD / 回归用例区
- 验收入口与环境身份校验区
- 人工验收指南的每一行（检查点 / 操作步骤 / 预期效果）

### B.3 为每个验证项选择测试引擎

| 验证项性质 | 默认引擎 | 选择理由 |
|---|---|---|
| UI 交互 / 视觉 / 控制台 | browser-use:browser（Codex 可用时优先）或 Playwright | 需要真实浏览器和用户视角截图 |
| API / 数据 / 业务逻辑 | curl / 代码单元测试 | 更快更直接 |
| 响应式 / 可访问性 | Playwright | 专业断言库 |
| 静态代码属性（文件存在 / import 正确） | Grep / Bash | 无需运行时 |

### B.4 执行验证 + 回填

对每条验证项：

1. 按"人工验收指南"的"怎么操作"执行
2. 每个有意义的状态节点截图：打开页面、操作前、操作后、加载态、结果态、错误态
3. 按"预期效果"做深度断言
4. 截图保存到 `docs/bugfix/reviews/assets/${BUG_ID}/`，并在 Markdown 中用 `![](...)` 内嵌
5. 回填"QA 测试过程与截图证据"节和"验收入口与环境身份校验"节

在 Codex 环境中，前端验证默认用 `browser-use:browser` 采集截图和 DOM 证据；需要更强可重复性时，再补 Playwright 脚本断言。若 Browser Use 不可用或被用户/插件中断，报告必须写明 fallback 原因。

截图命名建议：

```text
docs/bugfix/reviews/assets/${BUG_ID}/qa-1-01-open-page.png
docs/bugfix/reviews/assets/${BUG_ID}/qa-1-02-click-submit.png
docs/bugfix/reviews/assets/${BUG_ID}/qa-1-03-final-state.png
```

报告中必须写成：

```markdown
![](./assets/BF-0419-2/qa-1-01-open-page.png)
```

**断言原则**（和 Mode A 一致）：
- 必须基于"用户视角可见的内容变化"
- 不得单独用技术指标（HTTP 200 数量 / DOM 节点存在）
- 每个测试至少包含一个内容、状态、URL、CSS、网络响应或数据变化断言
- 必须核对前后端进程身份（优先看 `dev:status` / `dev-stack status`；兜底用 `ps aux | grep <服务>` + `lsof -p $PID | grep cwd`）
- QA 使用的 Frontend/Backend 地址必须和报告中交给用户验收的地址一致

### B.5 控制台零容忍（强制）

任何 `pageerror` 或 `console.error` 自动标记为 FAIL，即使该验证项的主逻辑通过。

### B.6 环境身份强校验

forge-qa 必须在报告的"验收入口与环境身份校验"区写入：

- Frontend URL、来源、PID、cwd、branch/commit（能获取时）
- Backend/API URL、来源、PID、cwd、branch/commit（涉及后端时）
- API health 或关键接口探活结果（涉及后端时）
- QA 执行时间
- 环境一致性结论：PASS / FAIL / EXPIRED

硬性 FAIL 条件：

- QA 实际访问的 URL 与报告交给用户验收的 URL 不一致
- 能拿到 PID/cwd，但 cwd 不属于当前 worktree
- 前端页面来自旧进程或主仓库，而不是当前 bug worktree
- 涉及后端但 Backend/API 地址无法确认
- 交给用户前再次检查发现地址或进程身份已变化

### B.7 回填"QA 测试过程与截图证据"节

forge-qa 必须填充报告里的"## QA 测试过程与截图证据（forge-qa 填）"：

```markdown
## QA 测试过程与截图证据（forge-qa 填）

**模式**：Mode B（单 bug 修复回归）
**执行时间**：2026-04-19 15:45

**自动化测试范围**：
- 跑了 Playwright 重放（3 步：打开登录 / 登录 / 查看头像）
- 跑了 tests/auth.test.ts（2 个相关 case）
- 控制台检查：0 error, 0 warning

### 验证项 1：登录后头像刷新

**结论**：PASS

**操作轨迹与截图**

1. 打开登录页

   ![](./assets/BF-0419-2/qa-1-01-open-login.png)

2. 提交登录

   ![](./assets/BF-0419-2/qa-1-02-submit-login.png)

3. 检查右上角头像

   ![](./assets/BF-0419-2/qa-1-03-avatar-updated.png)

**断言**
- 头像元素可见：PASS
- 头像 URL 已更新：PASS
- console.error：0
- network error：0

**Bug 复现核对**：
- 修复前：重现了 BF-0419-2 的原始症状（已对比 before 截图）
- 修复后：原始症状消失（after 截图）
```

### B.8 QA 自动闭环状态信号和退出

- **所有验证项 PASS 且环境一致性 PASS**：
  ```
  ✅ QA_PASS (BF-0419-2)
  报告已回填：docs/bugfix/reviews/BF-0419-2.md
  下一步：交还 forge-bugfix。单 bug 模式进入 P6.5；批量模式进入 qa-pass-pending-final-review。
  ```

- **至少一条 FAIL 或环境一致性 FAIL**：
  ```
  ❌ QA_FAIL (BF-0419-2)
  失败项: 第 3 条（控制台 TypeError）
  报告已回填 FAIL + 截图/日志证据。
  下一步：交还 forge-bugfix，有界回 P5 继续修复。
  ```

- **需求/设计/环境身份无法判断**：
  ```
  ⚠️ BLOCKED_HUMAN (BF-0419-2)
  原因: 保存后是否必须 toast 提示，Feature Spec 未定义。
  报告已写入决策卡。
  下一步：交还 forge-bugfix，询问用户。
  ```

### B.9 Mode B 不做的事

明确禁止：
- ❌ 不生成 `docs/QA.md`（那是 Mode A 的产物）
- ❌ 不做 User Gate（那由 forge-bugfix 的 P6.5 做）
- ❌ 不写 FEEDBACK.md（那是 Mode A 的 reject 闭环）
- ❌ 不主动判断"产品上是否接受"（只按报告和 Feature Spec 断言，最终由用户/批次验收决定）
- ❌ 不修改代码（和 Mode A 一样，只测不修）

### B.10 Mode B 的铁律

1. **只填 Bug 修复验收报告，不新建散乱 QA 文档**
2. **每条验证项必须有 pass/fail**（和 Mode A 第 3 条铁律一致）
3. **每个前端交互关键步骤必须有 Markdown 内嵌截图**
4. **控制台零容忍**（一致）
5. **不能越界修改用户验收列和最终结论**
6. **应用 URL 必须由调用方或 dev-status 提供**，不得在 Mode B 中猜测本地端口。
7. **环境身份校验失败就是 QA_FAIL**，不能用“页面能打开”替代。
8. **发现疑似新需求、范围外问题或设计歧义时输出 BLOCKED_HUMAN**，不要替用户决定。
9. **Codex 中优先使用 browser-use:browser 做本地浏览器验收**；Computer Use 不是浏览器首选兜底。
