feat(req): 部署门禁制度 — PDV 验收任务 + Deploy Gate 1-3

在 /req deploy 流程中增加部署后 E2E 验收(Post-Deploy Verification)门禁:
- 新增 verification linkRole 和【验收】任务命名规范
- Deploy Gate 1 健康检查 / Gate 2 PDV 任务完成 / Gate 3 证据完整
- PDV Playwright spec 模板(页面可达、菜单可见、API 连通)
- 同步更新 req-workflow、dev-test、e2e-testing 相关文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 09:34:08 +10:30
parent b9c808cce0
commit 187f5621c9
5 changed files with 270 additions and 16 deletions

View File

@@ -0,0 +1,147 @@
# PDV Smoke Spec 模板
部署后验收 (Post-Deploy Verification) Playwright 测试模板。AI 根据需求变更范围,基于此模板动态生成验收 spec。
## 使用方式
```bash
# 指定部署环境 URL 执行
E2E_BASE_URL=https://staging.example.com npx playwright test e2e/pdv/ --project=chromium
```
## Spec 模板结构
```typescript
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3000';
test.describe('PDV: {需求标题}', () => {
test.beforeEach(async ({ page }) => {
// 方式 1: 使用 storageState推荐需预先保存登录状态
// test.use({ storageState: 'e2e/.auth/user.json' });
// 方式 2: 手动登录
await page.goto(`${BASE_URL}/login`);
await page.fill('input[name="username"]', '{测试账号}');
await page.fill('input[name="password"]', '{测试密码}');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard/**');
});
test('菜单可见性: {菜单名}', async ({ page }) => {
await page.goto(`${BASE_URL}/`);
await page.waitForSelector('.ant-menu');
// 检查侧栏包含新菜单项
const menu = page.locator('.ant-menu');
await expect(menu).toContainText('{菜单名}');
// 截图证据
await page.screenshot({ path: 'e2e-results/pdv-menu-{菜单名}.png', fullPage: false });
});
test('页面可达: {页面路由}', async ({ page }) => {
const response = await page.goto(`${BASE_URL}{页面路由}`);
// 验证 HTTP 状态
expect(response?.status()).toBeLessThan(400);
// 验证非白屏 — title 不含错误关键词
await expect(page).not.toHaveTitle(/error|500|404|not found/i);
// 验证页面有核心内容(非空白)
await expect(page.locator('{核心选择器}')).toBeVisible({ timeout: 10000 });
// 检查无 JS 报错(通过 console error 监听)
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.waitForTimeout(2000);
expect(errors.filter(e => !e.includes('favicon'))).toHaveLength(0);
// 截图证据
await page.screenshot({ path: 'e2e-results/pdv-page-{页面名}.png', fullPage: true });
});
test('API 连通: {接口描述}', async ({ request }) => {
// 需要带认证 token 调用
const resp = await request.get(`${BASE_URL}/api/v1/{路径}`, {
headers: {
'Authorization': 'Bearer {token}',
},
});
// 验证非 5xx 错误
expect(resp.status()).toBeLessThan(500);
// 可选:验证响应结构
// const body = await resp.json();
// expect(body).toHaveProperty('data');
});
});
```
## 占位符说明
| 占位符 | 含义 | 来源 |
|--------|------|------|
| `{需求标题}` | 需求名称 | `ai-proj req get --id <id>` |
| `{菜单名}` | 新增的菜单文本 | 从需求关联的前端任务中提取 |
| `{页面路由}` | 新增/变更的前端路由 | 从前端路由配置或 PRD 提取 |
| `{核心选择器}` | 页面核心内容的 CSS 选择器 | 如 `h1`, `.page-title`, `[data-testid="xxx"]` |
| `{测试账号}` / `{测试密码}` | 测试环境登录凭据 | 环境配置 |
| `{token}` | API 认证 token | 登录后获取 |
| `{接口描述}` / `{路径}` | 关键 API 端点 | 从后端路由或 PRD 提取 |
| `{页面名}` | 截图文件名标识 | 自定义 |
## 生成示例OKR 功能)
```typescript
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3000';
test.describe('PDV: OKR 团队/对齐/设置/评分功能', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${BASE_URL}/login`);
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'TestPass123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard/**');
});
test('菜单可见性: OKR', async ({ page }) => {
await page.goto(`${BASE_URL}/`);
await page.waitForSelector('.ant-menu');
await expect(page.locator('.ant-menu')).toContainText('OKR');
await page.screenshot({ path: 'e2e-results/pdv-menu-okr.png' });
});
test('页面可达: /okr/my', async ({ page }) => {
const response = await page.goto(`${BASE_URL}/okr/my`);
expect(response?.status()).toBeLessThan(400);
await expect(page).not.toHaveTitle(/error|500|404/i);
await expect(page.locator('h1, .page-title')).toBeVisible({ timeout: 10000 });
await page.screenshot({ path: 'e2e-results/pdv-page-okr-my.png', fullPage: true });
});
test('页面可达: /okr/team', async ({ page }) => {
const response = await page.goto(`${BASE_URL}/okr/team`);
expect(response?.status()).toBeLessThan(400);
await expect(page).not.toHaveTitle(/error|500|404/i);
await expect(page.locator('h1, .page-title')).toBeVisible({ timeout: 10000 });
await page.screenshot({ path: 'e2e-results/pdv-page-okr-team.png', fullPage: true });
});
test('API 连通: OKR objectives', async ({ request }) => {
const resp = await request.get(`${BASE_URL}/api/v1/okr/objectives`);
expect(resp.status()).toBeLessThan(500);
});
});
```