sillyspec 3.7.2 → 3.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.7.2",
3
+ "version": "3.7.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "type": "module",
6
6
  "bin": {
package/src/setup.js CHANGED
@@ -1,10 +1,22 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { execSync } from 'child_process';
4
4
  import chalk from 'chalk';
5
5
  import ora from 'ora';
6
6
  import { checkbox, confirm, input, select } from '@inquirer/prompts';
7
7
 
8
+ // ── Skill 定义 ──
9
+
10
+ const SKILLS = [
11
+ {
12
+ id: 'playwright-e2e',
13
+ name: 'Playwright E2E 测试参考',
14
+ description: 'E2E 测试编写最佳实践,AI 执行测试任务时自动读取',
15
+ source: join(__dirname, '..', 'templates', 'skills', 'playwright-e2e'),
16
+ target: '.sillyspec/skills/playwright-e2e',
17
+ },
18
+ ];
19
+
8
20
  // ── MCP 工具定义 ──
9
21
 
10
22
  const MCP_TOOLS = [
@@ -266,6 +278,12 @@ export async function cmdSetup(dir, options = {}) {
266
278
  ...dbChoices,
267
279
  ...globalChoices.length > 0 ? [{ name: chalk.bold('── 全局工具 ──'), value: '_global_header', disabled: true }] : [],
268
280
  ...globalChoices,
281
+ ...[{ name: chalk.bold('── AI Skills(编写参考)──'), value: '_skill_header', disabled: true }],
282
+ ...SKILLS.filter(s => !existsSync(join(dir, s.target, 'SKILL.md'))).map(s => ({
283
+ name: `${s.name} — ${s.description}`,
284
+ value: `skill:${s.id}`,
285
+ checked: false,
286
+ })),
269
287
  ];
270
288
 
271
289
  if (allChoices.length === 0) {
@@ -355,6 +373,24 @@ export async function cmdSetup(dir, options = {}) {
355
373
  }
356
374
  }
357
375
 
376
+ // ── 安装 Skills ──
377
+
378
+ const selectedSkills = SKILLS.filter(s => selected.includes(`skill:${s.id}`));
379
+
380
+ if (selectedSkills.length > 0) {
381
+ console.log('');
382
+ for (const skill of selectedSkills) {
383
+ const spinner = ora(`安装 ${skill.name}...`).start();
384
+ try {
385
+ const targetDir = join(dir, skill.target);
386
+ cpSync(skill.source, targetDir, { recursive: true });
387
+ spinner.succeed(`${skill.name} 安装完成 → ${skill.target}/SKILL.md`);
388
+ } catch (err) {
389
+ spinner.fail(`${skill.name} 安装失败: ${err.message}`);
390
+ }
391
+ }
392
+ }
393
+
358
394
  // ── 总结 ──
359
395
 
360
396
  console.log('');
@@ -374,6 +410,10 @@ export async function cmdSetup(dir, options = {}) {
374
410
  console.log(` 🛠️ ${chalk.cyan(g.name)} — ${g.description}`);
375
411
  console.log(chalk.dim(` ${g.url}`));
376
412
  }
413
+ for (const s of selectedSkills) {
414
+ console.log(` 📚 ${chalk.cyan(s.name)} — ${s.description}`);
415
+ console.log(chalk.dim(` → ${s.target}/SKILL.md`));
416
+ }
377
417
  console.log('');
378
418
  if (selectedMcp.length > 0) {
379
419
  console.log(chalk.dim(' 重启你的 AI 工具以使 MCP 配置生效。'));
@@ -54,12 +54,97 @@ cat .sillyspec/knowledge/INDEX.md 2>/dev/null
54
54
 
55
55
  子代理遇到不熟悉的库或 API 时,优先使用已配置的 MCP 工具(Context7 等)或 web search 查最新文档,不要凭记忆猜测用法。
56
56
 
57
- **MCP 能力检测(主代理执行):**
57
+ **编码规范扫描(主代理执行):**
58
+ 主代理在 dispatch 子代理前,扫描项目中常见的编码规范配置文件,将关键规则注入子代理 prompt 的「编码规范约束」段。
59
+
58
60
  ```bash
59
- for cfg in .claude/mcp.json .cursor/mcp.json; do
60
- [ -f "$cfg" ] && echo "=== $cfg ===" && cat "$cfg"
61
+ # 检测存在的配置文件
62
+ for f in .eslintrc .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml \
63
+ .prettierrc .prettierrc.js .prettierrc.json .prettierrc.yml \
64
+ tsconfig.json tsconfig.base.json \
65
+ .editorconfig \
66
+ .tailwind.config.js .tailwind.config.ts \
67
+ .stylelintrc .stylelintrc.js .stylelintrc.json \
68
+ CONTRIBUTING.md CODE_STYLE.md; do
69
+ [ -f "$f" ] && echo "=== $f ===" && cat "$f"
61
70
  done
71
+ # 也检查 package.json 中的 lint/format 脚本
72
+ cat package.json 2>/dev/null | grep -A5 '"lint\|"format\|"typecheck\|"type-check'
73
+ ```
74
+
75
+ 扫描后,主代理根据检测结果生成**编码规范摘要**(不是原文粘贴,是提炼关键约束),格式如下。如果某个类别未检测到对应配置文件,则省略该段:
76
+
77
+ ```
78
+ ## 编码规范约束(自动扫描)
79
+
80
+ ### ESLint
81
+ {提取的关键规则,如:禁止 var、要求分号、禁止未使用变量、特定插件规则等}
82
+
83
+ ### Prettier
84
+ {提取的格式化规则,如:单引号、2空格缩进、无分号、行宽 80 等}
85
+
86
+ ### TypeScript
87
+ {从 tsconfig 提取的严格模式设置,如:strict: true、noUncheckedIndexedAccess 等}
88
+
89
+ ### Import / 命名约定
90
+ {从 eslint/import 插件或 editorconfig 提取的导入排序、命名风格等}
91
+
92
+ ### 框架约定
93
+ {检测到的框架特定约定,如:Next.js App Router、Tailwind 类名风格等}
94
+ ```
95
+
96
+ 将此摘要注入到每个子代理 prompt 的「项目约定」段之后,并追加一条铁律:
97
+ > **10. 遵守编码规范:** 以上「编码规范约束」中的所有规则必须严格遵守。如规则与任务描述冲突,优先遵守规范约束并报告。
98
+
99
+ **测试模式扫描(主代理执行):**
100
+ 对包含 E2E/测试任务时,扫描项目已有的测试文件,提取测试风格注入子代理 prompt 的「测试模式参考」段。
101
+
102
+ ```bash
103
+ # 检测测试框架
104
+ cat package.json 2>/dev/null | grep -E "playwright|cypress|jest|vitest|mocha"
105
+
106
+ # 查找已有测试文件
107
+ find . -name "*.spec.ts" -o -name "*.test.ts" -o -name "*.spec.tsx" -o -name "*.spec.js" \
108
+ -o -name "playwright.config.*" -o -name "vitest.config.*" -o -name "jest.config.*" \
109
+ 2>/dev/null | grep -v node_modules | head -10
110
+
111
+ # 读取 1-2 个已有测试文件作为风格参考
112
+ # 优先读 E2E 测试,其次读通用测试
113
+ find tests/e2e e2e cypress/e2e __tests__ src -name "*.spec.ts" -o -name "*.test.ts" \
114
+ 2>/dev/null | grep -v node_modules | head -3 | xargs cat 2>/dev/null
115
+ ```
116
+
117
+ 扫描后生成**测试模式摘要**:
118
+
62
119
  ```
120
+ ## 测试模式参考(自动扫描)
121
+
122
+ ### 测试框架
123
+ {检测到的框架及版本,如:Playwright 1.42、Vitest 1.2}
124
+
125
+ ### 断言风格
126
+ {从已有测试提取的断言模式,如:expect(page).toHaveText()、toEqual、toBeTruthy 等}
127
+
128
+ ### Fixtures / Helper
129
+ {项目自定义的 test fixtures、page objects、helper 函数}
130
+
131
+ ### 文件组织
132
+ {测试文件目录结构、命名约定、文件内组织方式}
133
+
134
+ ### 配置要点
135
+ {playwright.config.ts 中的 baseURL、timeout、retries 等关键配置}
136
+ ```
137
+
138
+ 将此摘要注入到每个 E2E/测试子代理 prompt 的「任务描述」段之后,并追加一条铁律:
139
+ > **11. 参照已有测试风格:** 编写新测试时必须参照以上「测试模式参考」中的风格,包括断言方式、fixtures 使用、文件组织。不要凭记忆写测试。
140
+ > **12. 参考已有实现:** 写新功能前,先 grep 项目中类似功能的已有代码(`grep -r "关键词" src/`),照着项目现有的模式、风格和封装方式写,不要凭记忆编造。
141
+
142
+ **MCP 能力检测(主代理执行):**
143
+ 检查当前可用工具列表中是否存在以下类型的 MCP 工具(不要只依赖配置文件路径,不同客户端配置位置不同):
144
+ - Context7 / 文档查询工具
145
+ - 数据库工具(postgres/sqlite/mysql/redis)
146
+ - 浏览器工具(browser/chrome/playwright/devtools)
147
+ - 搜索工具(search/web_search)
63
148
  根据检测结果,在子代理 prompt 的「文档查询指引」段动态注入:
64
149
  - 有 `context7` → `遇到不熟悉的库/API,使用 Context7 MCP(resolve-library-id → query-docs)查询最新文档`
65
150
  - 无 `context7` → `遇到不熟悉的库/API,使用 web search 查询最新官方文档`
@@ -103,6 +188,12 @@ done
103
188
  ## 项目约定
104
189
  {CONVENTIONS.md 全文}
105
190
 
191
+ ## 编码规范约束(自动扫描)
192
+ {主代理扫描项目配置文件后生成的规范摘要,见上方「编码规范扫描」步骤}
193
+
194
+ ## 测试模式参考(自动扫描,仅 E2E/测试任务注入)
195
+ {主代理扫描项目已有测试文件后生成的测试风格摘要,见上方「测试模式扫描」步骤。非 E2E/测试任务省略此段}
196
+
106
197
  ## 项目架构
107
198
  {ARCHITECTURE.md 全文}
108
199
 
@@ -129,9 +220,15 @@ done
129
220
  4. **不自行补全:** 发现缺失接口/方法,不自己写,报告 BLOCKED
130
221
  5. **TDD 不跳步:** 按任务步骤逐步执行,每步必须运行测试命令并确认结果
131
222
  6. **测试直接通过 = 测了已有行为,重写测试**
132
- 7. **E2E 任务:** 如果任务描述包含"E2E"或"端到端",先 cat 相关功能代码和页面组件,理解交互逻辑。有测试框架则编写测试文件,无框架则编写 `.sillyspec/changes/<变更名>/e2e-steps.md` 结构化测试步骤(每条包含操作和断言)
223
+ 7. **E2E 任务:** 如果任务描述包含"E2E"或"端到端"
224
+ - 先 cat 相关功能代码和页面组件,理解交互逻辑
225
+ - 参考 prompt 中「测试模式参考」段的已有测试风格
226
+ - **查阅 Playwright 用法:** 优先使用已安装的 playwright skill(SKILL.md),不要凭记忆写 API。未安装则通过 Context7 MCP 或 web search 查最新文档
227
+ - 有测试框架则编写测试文件,无框架则编写 `.sillyspec/changes/<变更名>/e2e-steps.md` 结构化测试步骤
228
+ - **写完必须立即跑一遍确认通过**,失败则修复后重跑,不要"写了就算完成"
133
229
  8. **暂存:** 完成后在工作目录执行 git add -A(不要 commit,由用户通过 /sillyspec:commit 统一提交)
134
230
  9. **不修改计划外的文件**,如必须修改则在报告中说明
231
+ 10. **遵守编码规范:** prompt 中「编码规范约束」段的所有规则必须严格遵守。如规范与任务描述冲突,优先遵守规范并报告
135
232
 
136
233
  ## 完成后报告(严格按此格式)
137
234
 
@@ -166,6 +263,20 @@ done
166
263
 
167
264
  ## 完成后
168
265
 
266
+ **任务勾选自检(必须执行):**
267
+ 所有任务完成后,主代理必须执行以下检查:
268
+
269
+ ```bash
270
+ cat .sillyspec/changes/*/tasks.md 2>/dev/null
271
+ ```
272
+
273
+ 逐条验证:
274
+ 1. **所有返回 DONE/DONE_WITH_CONCERNS 的任务是否已勾选 `- [x]`?**
275
+ 2. **勾选的任务是否都记录了精确到秒的时间戳 `[YYYY-MM-DD HH:MM:SS]`?**
276
+ 3. **tasks.md 中是否还有未勾选 `- [ ]` 的已完成任务?**
277
+
278
+ 发现遗漏 → 立即补勾选 + 补时间戳,不要等用户提醒。
279
+
169
280
  **知识库审阅:** 检查是否有待确认的知识条目:
170
281
  ```bash
171
282
  grep -c '^\### \[待确认\]' .sillyspec/knowledge/uncategorized.md 2>/dev/null
@@ -29,10 +29,32 @@ $ARGUMENTS
29
29
  ```bash
30
30
  ls .sillyspec/changes/ 2>/dev/null | grep -v archive
31
31
  cat .sillyspec/{REQUIREMENTS,ROADMAP}.md 2>/dev/null
32
+ cat .sillyspec/codebase/{CONVENTIONS,ARCHITECTURE}.md 2>/dev/null
33
+ cat .sillyspec/knowledge/INDEX.md 2>/dev/null
32
34
  ```
33
35
 
34
36
  有进行中变更时读取其 design/tasks,自然引用。发现重要决策时可提议保存(不自动保存)。
35
37
 
38
+ ## MCP 能力(按需使用)
39
+
40
+ 检查当前可用工具列表中是否存在 MCP 工具(Context7/浏览器/数据库/搜索等),不依赖配置文件路径。
41
+
42
+ - 有 Context7 → 探索时查询最新文档,验证技术方案的可行性
43
+ - 有浏览器 MCP → 可浏览相关网站、查竞品实现
44
+ - 有搜索 MCP → 搜索技术方案、最佳实践
45
+ - 无 MCP → 使用 web search
46
+
47
+ ## 话题升级提示
48
+
49
+ 探索过程中,当对话达到一定深度时(讨论了 5+ 轮、或涉及具体实现方案、或用户表达"试试看"/"能不能做"/"怎么搞"),主动用 AskUserQuestion 提示用户:
50
+
51
+ 1. **🧠 头脑风暴** — `/sillyspec:brainstorm` 深度探索需求和方案
52
+ 2. **⚡ 快速执行** — `/sillyspec:quick` 直接动手做
53
+ 3. **📋 创建规范** — `/sillyspec:propose` 生成结构化规范
54
+ 4. **🔍 继续探索** — 还没聊透,继续
55
+
56
+ 不需要每次都提示,只在对话**明显转向执行意图**时触发。纯讨论、提问、查资料时不要打断。
57
+
36
58
  ## 没有必需的结束
37
59
 
38
60
  探索可以创建变更提案、产出文档、继续探索或结束。
package/templates/plan.md CHANGED
@@ -123,6 +123,30 @@ cat .claude/mcp.json .cursor/mcp.json 2>/dev/null | grep -i "browser\|chrome\|de
123
123
  - [ ] 每个 task 有具体文件路径?
124
124
  - [ ] 标注了 Wave 和依赖关系?
125
125
  - [ ] 涉及 UI 的任务是否有对应的 E2E 测试任务?
126
+ - [ ] **design.md 中的每个功能点是否都在 tasks.md 中有对应任务?**
127
+
128
+ ### 6.5 设计完整性对照(必须完成)
129
+
130
+ 逐条检查 design.md 中的功能描述,确保每个功能点都有对应的 task。特别关注:
131
+
132
+ 1. **逐功能点扫描:** 将 design.md 中描述的每个功能点(含子功能)列出,与 tasks.md 逐条对照
133
+ 2. **前后端覆盖检查:** 涉及前后端协作的功能,确认前端和后端各有独立 task
134
+ 3. **遗漏项处理:** 发现未覆盖的功能点 → 追加 task 到对应 Wave,并提示用户确认
135
+
136
+ **执行方式:**
137
+ ```
138
+ 从 design.md 提取功能点清单:
139
+ 功能 A(后端接口)
140
+ 功能 B(前端页面)
141
+ 功能 C(前后端联动)
142
+
143
+ 与 tasks.md 对照:
144
+ ✅ 功能 A → task-3
145
+ ❌ 功能 B → 无对应 task → 追加
146
+ ⚠️ 功能 C → 只有后端 task,缺少前端 task → 追加
147
+ ```
148
+
149
+ 发现遗漏时用 AskUserQuestion 确认追加内容,用户确认后再写入 tasks.md。
126
150
 
127
151
  ### 7. 完成
128
152
 
@@ -21,12 +21,19 @@ $ARGUMENTS
21
21
  1. **解析参数:** 检查是否携带 `--change <变更名>`,确定记录方式
22
22
  2. **理解任务:** 模糊则问一个问题确认
23
23
  3. **加载上下文:** `cat .sillyspec/codebase/{CONVENTIONS,ARCHITECTURE}.md 2>/dev/null`
24
+ 3b. **编码规范扫描:** 检测项目中的编码规范配置文件(`.eslintrc*`、`.prettierrc*`、`tsconfig.json`、`.editorconfig`、`tailwind.config.*`、`CONTRIBUTING.md`),提取关键规则生成摘要。写作代码时必须严格遵守这些规则(分号/引号/缩进/命名风格等),如不确定优先遵守规范约束。
24
25
  4. **知识库查询(强制步骤):**
25
26
  ```bash
26
27
  cat .sillyspec/knowledge/INDEX.md 2>/dev/null
27
28
  ```
28
29
  根据当前任务描述中的关键词匹配 INDEX.md 条目,命中时 `cat` 对应知识文件,将内容纳入后续开发考量。未命中则跳过。
29
- **MCP 检测:** `cat .claude/mcp.json .cursor/mcp.json 2>/dev/null`,有 Context7 则用 MCP 查文档,无则用 web search。
30
+ **MCP 检测:** 检查当前可用工具列表中是否存在 MCP 工具(Context7/浏览器/数据库/搜索等),根据检测结果动态利用:
31
+ - 有 Context7 → 查询不熟悉库/API 的最新文档
32
+ - 有浏览器 MCP → 验证页面改动效果
33
+ - 有数据库 MCP → 查询表结构和数据验证(只读)
34
+ - 有搜索 MCP → 搜索最佳实践和解决方案
35
+ - 有其他 MCP → 按任务需要灵活使用
36
+ - 无 MCP → 使用 web search
30
37
  5. **先读后写:** 调用已有方法前 `cat` 源文件确认签名,`grep` 确认方法存在
31
38
  6. **数据操作安全:** 任何改变现有数据的操作(非 SELECT 的数据库操作)必须暂停并报告给用户确认,不得自动执行。新建表不受此限制
32
39
  6. **TDD 执行:**
@@ -0,0 +1,340 @@
1
+ ---
2
+ name: playwright-e2e
3
+ description: Playwright E2E 测试编写参考。AI 执行 E2E 任务时自动读取,不要凭记忆编写 Playwright API。
4
+ ---
5
+
6
+ # Playwright E2E 测试编写参考
7
+
8
+ > ⚠️ 执行 E2E/测试任务时必须先读取此文件,不要凭训练数据记忆编写 Playwright API。
9
+
10
+ ## 测试基本结构
11
+
12
+ ```typescript
13
+ import { test, expect } from '@playwright/test';
14
+
15
+ test.describe('功能名称', () => {
16
+ test.beforeEach(async ({ page }) => {
17
+ await page.goto('/');
18
+ });
19
+
20
+ test('具体场景', async ({ page }) => {
21
+ // Arrange
22
+ // Act
23
+ // Assert
24
+ });
25
+ });
26
+ ```
27
+
28
+ ## 选择器优先级
29
+
30
+ ```typescript
31
+ // ✅ 最推荐:data-testid(最稳定)
32
+ await page.locator('[data-testid="submit-btn"]').click();
33
+
34
+ // ✅ 推荐:role-based(语义化)
35
+ await page.getByRole('button', { name: 'Submit' }).click();
36
+ await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
37
+
38
+ // ✅ 推荐:文本匹配
39
+ await page.getByText('Sign in').click();
40
+ await page.getByText(/welcome back/i).click();
41
+
42
+ // ✅ 可用:语义 HTML
43
+ await page.locator('button[type="submit"]').click();
44
+ await page.locator('input[name="email"]').fill('test@test.com');
45
+
46
+ // ❌ 避免:CSS 类名和 ID(重构易变)
47
+ // await page.locator('.btn-primary').click();
48
+ // await page.locator('#submit').click();
49
+ ```
50
+
51
+ ## 断言
52
+
53
+ ```typescript
54
+ // URL
55
+ await expect(page).toHaveURL('/dashboard');
56
+ await expect(page).toHaveURL(/.*dashboard/);
57
+
58
+ // 文本
59
+ await expect(page.locator('h1')).toHaveText('Welcome');
60
+ await expect(page.locator('.message')).toContainText('success');
61
+ await expect(page.locator('.items')).toHaveText(['Item 1', 'Item 2']);
62
+
63
+ // 可见性
64
+ await expect(page.locator('.modal')).toBeVisible();
65
+ await expect(page.locator('.spinner')).toBeHidden();
66
+ await expect(page.locator('button')).toBeEnabled();
67
+ await expect(page.locator('input')).toBeDisabled();
68
+
69
+ // 数量
70
+ await expect(page.locator('.item')).toHaveCount(3);
71
+
72
+ // 输入值
73
+ await expect(page.locator('input')).toHaveValue('test@example.com');
74
+ ```
75
+
76
+ ## 表单操作
77
+
78
+ ```typescript
79
+ // 文本输入
80
+ await page.getByLabel('Email').fill('user@example.com');
81
+ await page.getByPlaceholder('Enter your name').fill('John Doe');
82
+
83
+ // 清空再输入
84
+ await page.locator('#username').clear();
85
+ await page.locator('#username').type('newuser', { delay: 100 });
86
+
87
+ // 复选框
88
+ await page.getByLabel('I agree').check();
89
+ await page.getByLabel('I agree').uncheck();
90
+
91
+ // 单选按钮
92
+ await page.getByLabel('Option 2').check();
93
+
94
+ // 下拉选择
95
+ await page.selectOption('select#country', 'usa');
96
+ await page.selectOption('select#country', { label: 'United States' });
97
+
98
+ // 多选下拉
99
+ await page.selectOption('select#colors', ['red', 'blue']);
100
+
101
+ // 文件上传
102
+ await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
103
+ await page.setInputFiles('input[type="file"]', ['file1.pdf', 'file2.pdf']);
104
+ ```
105
+
106
+ ## 鼠标与键盘
107
+
108
+ ```typescript
109
+ // 点击
110
+ await page.click('button');
111
+ await page.click('button', { button: 'right' }); // 右键
112
+ await page.dblclick('button'); // 双击
113
+
114
+ // 悬停
115
+ await page.hover('.menu-item');
116
+
117
+ // 拖拽
118
+ await page.dragAndDrop('#source', '#target');
119
+
120
+ // 键盘
121
+ await page.keyboard.type('Hello', { delay: 100 });
122
+ await page.keyboard.press('Control+A');
123
+ await page.keyboard.press('Enter');
124
+ await page.keyboard.press('Tab');
125
+ ```
126
+
127
+ ## 测试数据准备
128
+
129
+ ```typescript
130
+ // ⚠️ E2E 测试不能因为"没有数据"就跳过!必须确保测试数据存在。
131
+
132
+ // 方案 1:通过 API 创建前置数据(推荐)
133
+ test.beforeEach(async ({ request }) => {
134
+ // 调用项目 API 创建测试所需数据
135
+ const resp = await request.post('/api/test/setup', {
136
+ data: { createUser: true, createOrder: true }
137
+ });
138
+ expect(resp.ok()).toBeTruthy();
139
+ });
140
+
141
+ // 方案 2:使用 storageState 复用认证状态
142
+ // 先登录一次保存状态,后续测试直接复用
143
+ test.use({ storageState: 'tests/auth/user.json' });
144
+
145
+ // 生成认证状态的脚本:
146
+ // npx playwright codegen --save-storage=tests/auth/user.json http://localhost:3000/login
147
+
148
+ // 方案 3:通过数据库 MCP 直接插入数据(有数据库 MCP 时)
149
+ // 在 beforeEach 中调用数据库 MCP 执行 INSERT 语句准备数据
150
+
151
+ // 方案 4:API Mock(不需要真实数据)
152
+ test('显示用户列表', async ({ page }) => {
153
+ await page.route('**/api/users', route => {
154
+ route.fulfill({
155
+ status: 200,
156
+ contentType: 'application/json',
157
+ body: JSON.stringify([{ id: 1, name: 'Test User' }])
158
+ });
159
+ });
160
+ await page.goto('/users');
161
+ await expect(page.locator('.user-name')).toHaveText('Test User');
162
+ });
163
+
164
+ // 清理:测试后清理数据,避免影响其他测试
165
+ test.afterEach(async ({ request }) => {
166
+ await request.post('/api/test/cleanup');
167
+ });
168
+ ```
169
+
170
+ **铁律:E2E 测试禁止因"没有数据"跳过。** 必须通过以上任一方案准备数据,确保测试可独立运行。
171
+
172
+ ## 弹窗与 iframe
173
+
174
+ ```typescript
175
+ // 新窗口/弹窗
176
+ const [popup] = await Promise.all([
177
+ page.waitForEvent('popup'),
178
+ page.click('button.open-popup'),
179
+ ]);
180
+ await popup.waitForLoadState();
181
+
182
+ // iframe
183
+ const frame = page.frameLocator('#my-iframe');
184
+ await frame.locator('button').click();
185
+ ```
186
+
187
+ ## 截图
188
+
189
+ ```typescript
190
+ // 全页截图
191
+ await page.screenshot({ path: 'screenshot.png', fullPage: true });
192
+
193
+ // 元素截图
194
+ await page.locator('.chart').screenshot({ path: 'chart.png' });
195
+
196
+ // 视觉回归对比(首次生成基线,后续对比)
197
+ await expect(page).toHaveScreenshot('homepage.png');
198
+
199
+ // ⚠️ playwright.config.ts 中配置失败自动截图:
200
+ // screenshot: 'only-on-failure'(已包含在上方 config 模板中)
201
+ ```
202
+
203
+ ## 等待策略
204
+
205
+ ```typescript
206
+ // ✅ 推荐:自动等待(Playwright 内置)
207
+ await expect(page.locator('.loaded')).toBeVisible();
208
+
209
+ // ✅ 推荐:等待 URL 变化
210
+ await page.waitForURL('**/dashboard');
211
+
212
+ // ✅ 推荐:等待网络响应
213
+ const responsePromise = page.waitForResponse('**/api/users');
214
+ await page.click('button#load-users');
215
+ const response = await responsePromise;
216
+
217
+ // ❌ 避免:硬编码 sleep
218
+ // await page.waitForTimeout(3000);
219
+ ```
220
+
221
+ ## Page Object Model
222
+
223
+ ```typescript
224
+ // pages/LoginPage.ts
225
+ export class LoginPage {
226
+ constructor(private page: Page) {}
227
+
228
+ async goto() {
229
+ await this.page.goto('/login');
230
+ }
231
+
232
+ async login(email: string, password: string) {
233
+ await this.page.getByLabel('Email').fill(email);
234
+ await this.page.getByLabel('Password').fill(password);
235
+ await this.page.getByRole('button', { name: 'Submit' }).click();
236
+ }
237
+ }
238
+
239
+ // tests/login.spec.ts
240
+ import { test, expect } from '@playwright/test';
241
+ import { LoginPage } from '../pages/LoginPage';
242
+
243
+ test('登录成功', async ({ page }) => {
244
+ const loginPage = new LoginPage(page);
245
+ await loginPage.goto();
246
+ await loginPage.login('test@example.com', 'password123');
247
+ await expect(page).toHaveURL('/dashboard');
248
+ });
249
+ ```
250
+
251
+ ## API Mock(隔离后端)
252
+
253
+ ```typescript
254
+ test('显示用户列表', async ({ page }) => {
255
+ await page.route('**/api/users', route => {
256
+ route.fulfill({
257
+ status: 200,
258
+ contentType: 'application/json',
259
+ body: JSON.stringify([{ id: 1, name: 'Test User' }])
260
+ });
261
+ });
262
+
263
+ await page.goto('/users');
264
+ await expect(page.locator('.user-name')).toHaveText('Test User');
265
+ });
266
+ ```
267
+
268
+ ## 常见错误
269
+
270
+ ### ❌ 用 CSS 类名选择元素
271
+ ```typescript
272
+ await page.click('.btn-primary'); // 脆弱!重构就坏
273
+ ```
274
+
275
+ ### ✅ 用 data-testid
276
+ ```typescript
277
+ await page.click('[data-testid="submit-btn"]'); // 稳定
278
+ ```
279
+
280
+ ### ❌ 测试之间有依赖
281
+ ```typescript
282
+ test('步骤1:登录', async ({ page }) => { /* ... */ });
283
+ test('步骤2:需要先登录', async ({ page }) => { /* 依赖步骤1,脆弱 */ });
284
+ ```
285
+
286
+ ### ✅ 每个测试独立
287
+ ```typescript
288
+ test.beforeEach(async ({ page }) => {
289
+ await page.goto('/login');
290
+ await page.getByLabel('Email').fill('test@example.com');
291
+ await page.getByLabel('Password').fill('password');
292
+ await page.getByRole('button', { name: 'Submit' }).click();
293
+ await expect(page).toHaveURL('/dashboard');
294
+ });
295
+ ```
296
+
297
+ ### ❌ 不处理异步状态
298
+ ```typescript
299
+ await page.click('[data-testid="submit"]');
300
+ await expect(page.locator('.result')).toBeVisible(); // 可能还没加载完
301
+ ```
302
+
303
+ ### ✅ 等待操作完成
304
+ ```typescript
305
+ await page.click('[data-testid="submit"]');
306
+ await expect(page.locator('.result')).toBeVisible({ timeout: 10000 });
307
+ ```
308
+
309
+ ## playwright.config.ts 模板
310
+
311
+ ```typescript
312
+ import { defineConfig } from '@playwright/test';
313
+
314
+ export default defineConfig({
315
+ testDir: './tests/e2e',
316
+ fullyParallel: true,
317
+ retries: process.env.CI ? 2 : 0,
318
+ timeout: 30000,
319
+ use: {
320
+ baseURL: 'http://localhost:3000', // ⚠️ 根据项目实际端口调整
321
+ trace: 'on-first-retry',
322
+ screenshot: 'only-on-failure',
323
+ },
324
+ webServer: {
325
+ command: 'npm run dev', // ⚠️ 根据项目实际命令调整
326
+ port: 3000,
327
+ reuseExistingServer: !process.env.CI,
328
+ },
329
+ });
330
+ ```
331
+
332
+ ## 调试命令
333
+
334
+ ```bash
335
+ npx playwright test --headed # 有头模式
336
+ npx playwright test --debug # 调试模式(带 inspector)
337
+ npx playwright test --slowmo=1000 # 慢放
338
+ npx playwright show-report # 查看报告
339
+ npx playwright codegen URL # 录制生成代码
340
+ ```
@@ -47,6 +47,7 @@ else
47
47
  CHANGE_DIR=$(ls -d .sillyspec/changes/*/ 2>/dev/null | grep -v archive | tail -1)
48
48
  fi
49
49
  cat "$CHANGE_DIR"/{design,tasks}.md 2>/dev/null
50
+ cat .sillyspec/local.yaml 2>/dev/null
50
51
  ```
51
52
 
52
53
  锚定确认实际存在的文件。
@@ -97,9 +98,49 @@ npx vitest run 2>/dev/null || npx jest 2>/dev/null
97
98
  > ⚠️ 使用 MCP 执行时 AI 判断可能不如测试框架精确。追求可靠性建议安装 Playwright。
98
99
 
99
100
  **自动修复循环(选了策略 1 或 2 时):**
100
- 读取 `.sillyspec/local.yaml` 中当前变更的 `fixAttempts`,对每个失败测试:
101
- - fixAttempts 未达上限 → 调 `/sillyspec:quick "修复 E2E 失败:<失败描述>"` → 重跑该测试 → 更新 local.yaml
102
- - fixAttempts 达到上限 → 停止,报告失败详情,提示人工介入
101
+
102
+ 必须按以下流程严格执行,不可跳过:
103
+
104
+ ```
105
+ ROUND = 1
106
+ MAX_ROUNDS = 策略1时为5,策略2时为50
107
+
108
+ while ROUND <= MAX_ROUNDS:
109
+ 1. 运行失败测试,捕获完整输出(错误信息、堆栈、期望值 vs 实际值)
110
+ 2. 如果全部通过 → 跳出循环,标记 ✅
111
+ 3. 读取 .sillyspec/local.yaml 中当前变更的 fixAttempts
112
+ 4. 对每个失败测试:
113
+ a. 如果 fixAttempts >= MAX_ROUNDS → 跳过,标记 ❌ MAX_REACHED
114
+ b. 否则 → 调 /sillyspec:quick 修复,prompt 必须包含:
115
+ - 失败的测试文件路径和测试名
116
+ - 完整错误信息(含期望值 vs 实际值)
117
+ - 相关源文件路径
118
+ - "只修复这个测试失败,不要改其他代码"
119
+ c. 修复后重跑该测试确认是否通过
120
+ d. 通过 → fixAttempts 保持不变;仍失败 → fixAttempts + 1
121
+ 5. 写入 .sillyspec/local.yaml 更新 fixAttempts
122
+ 6. ROUND++
123
+ 7. 如果本轮无任何修复(所有失败都已 MAX_REACHED)→ 跳出循环
124
+ ```
125
+
126
+ **quick 修复 prompt 模板:**
127
+ ```
128
+ /sillyspec:quick "修复测试失败:<测试文件路径>:<测试名>
129
+
130
+ 错误信息:
131
+ <完整错误输出,包含期望值和实际值>
132
+
133
+ 相关文件:
134
+ <被测源文件路径>
135
+
136
+ 只修复这个测试失败,不要改其他代码。修完后运行该测试确认通过。"
137
+ ```
138
+
139
+ **禁止行为:**
140
+ - ❌ 只看错误摘要就修复(必须看完整输出)
141
+ - ❌ 跳过 fixAttempts 计数
142
+ - ❌ 一次 quick 修复多个不相关的失败(逐个修复,每次修复后重跑确认)
143
+ - ❌ 主代理直接修改代码(verify 阶段禁止改代码,必须通过 /sillyspec:quick)
103
144
 
104
145
  **更新测试结果到 `.sillyspec/local.yaml`(按变更名隔离,覆盖写入):**
105
146
  ```yaml
@@ -121,7 +162,78 @@ grep -r "TODO\|FIXME\|HACK\|XXX" src/ lib/ app/ --include="*.ts" --include="*.ts
121
162
 
122
163
  审查 design.md「文件变更」中列出的文件:安全问题(输入校验、SQL拼接、硬编码敏感信息)、潜在 bug(空值、边界条件)、与 CONVENTIONS.md 一致性。每个问题标 🔴必须 / 🟡建议 / 🔵优化。
123
164
 
124
- ### 6. 输出验证报告
165
+ ### 5.5 MCP 基础设施验证
166
+
167
+ 检测已配置的 MCP 服务,利用它们做实际验证(不只查文档):
168
+
169
+ **MCP 能力检测:**
170
+
171
+ 不要只检查配置文件路径(不同客户端配置位置不同),直接检查当前可用工具列表中是否存在以下工具:
172
+
173
+ - 数据库相关工具(包含 postgres/sqlite/mysql/redis 关键词)
174
+ - 浏览器相关工具(包含 browser/chrome/puppeteer/playwright/devtools 关键词)
175
+ - 搜索相关工具(包含 search/web_search 关键词)
176
+
177
+ **判断方式:** 尝试调用或列出当前可用的 MCP 工具,有就用来验证,没有就跳过。
178
+
179
+ **按检测结果执行对应验证:**
180
+
181
+ **数据库 MCP(postgres/sqlite/mysql/redis):**
182
+ - 对照 design.md 中的数据模型,验证表/集合是否存在
183
+ - 验证字段类型、约束(主键、外键、唯一索引)是否与设计一致
184
+ - 验证新增的 API 是否能正确读写对应数据
185
+ - ⚠️ 只执行 SELECT 查询,禁止任何写操作
186
+
187
+ **浏览器 MCP(chrome-devtools/puppeteer/playwright):**
188
+ - 验证页面能否正常加载(无 404/500 错误)
189
+ - 验证关键 UI 元素是否存在(导航、表单、按钮等)
190
+ - 验证基础交互(点击、提交、跳转)
191
+
192
+ **API MCP:**
193
+ - 验证新增接口是否可达
194
+ - 验证请求/响应格式是否与设计一致
195
+
196
+ **无 MCP → 跳过此步骤,不影响验证结论。**
197
+
198
+ 将验证结果纳入验证报告。
199
+
200
+ ### 6. Lint / Format 检查
201
+
202
+ 自动检测并运行项目配置的 lint/format 工具,验证代码是否符合规范:
203
+
204
+ ```bash
205
+ # ESLint
206
+ if [ -f .eslintrc -o -f .eslintrc.js -o -f .eslintrc.cjs -o -f .eslintrc.json -o -f .eslintrc.yml ] || grep -q '"eslint"' package.json 2>/dev/null; then
207
+ echo "=== ESLint ==="
208
+ npx eslint . --max-warnings 0 2>&1 | tail -50
209
+ fi
210
+
211
+ # Prettier(检查而非修复)
212
+ if [ -f .prettierrc -o -f .prettierrc.js -o -f .prettierrc.json -o -f .prettierrc.yml ] || grep -q '"prettier"' package.json 2>/dev/null; then
213
+ echo "=== Prettier ==="
214
+ npx prettier --check . 2>&1 | tail -30
215
+ fi
216
+
217
+ # TypeScript 类型检查
218
+ if [ -f tsconfig.json ]; then
219
+ echo "=== TypeScript ==="
220
+ npx tsc --noEmit 2>&1 | tail -30
221
+ fi
222
+
223
+ # Stylelint
224
+ if [ -f .stylelintrc -o -f .stylelintrc.js -o -f .stylelintrc.json ] || grep -q '"stylelint"' package.json 2>/dev/null; then
225
+ echo "=== Stylelint ==="
226
+ npx stylelint "**/*.{css,scss,less}" 2>&1 | tail -30
227
+ fi
228
+ ```
229
+
230
+ **处理策略(AskUserQuestion):**
231
+ 1. **自动修复** — 对支持 `--fix` 的工具(ESLint、Prettier、Stylelint)自动修复后重跑检查,同一问题最多修复 3 次
232
+ 2. **只报告** — 仅列出所有 lint 错误,不修改代码
233
+
234
+ 将 lint 结果纳入验证报告(步骤 7)。
235
+
236
+ ### 7. 输出验证报告
125
237
 
126
238
  ```markdown
127
239
  # SillySpec 验证报告
@@ -130,6 +242,8 @@ grep -r "TODO\|FIXME\|HACK\|XXX" src/ lib/ app/ --include="*.ts" --include="*.ts
130
242
  ## 测试结果:passed N, failed N
131
243
  ## 技术债务标记
132
244
  ## 代码审查:🔴 N / 🟡 N / 🔵 N
245
+ ## Lint 检查:ESLint ✅/❌ | Prettier ✅/❌ | TypeScript ✅/❌ | Stylelint ✅/❌
246
+ ## MCP 基础设施验证:数据库 ✅/❌/跳过 | 页面 ✅/❌/跳过 | API ✅/❌/跳过
133
247
  ## E2E 测试:passed N / failed N / fixAttempts 详情
134
248
  ## 结论:✅ PASS / ⚠️ PASS WITH NOTES / ❌ FAIL
135
249
  ```