[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EGTZVC78J35KSRGMQX2FSS":3,"skill-same:01M6EGTZVC78J35KSRGMQX2FSS":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":33,"linkOnly":34,"markdownContent":35,"markdownBody":36,"frontMatter":37,"refName":39,"commitSha":40,"skillDirectory":41,"forksCount":42,"repositoryHtmlUrl":43,"repositoryHomepageUrl":44,"ownerHtmlUrl":45,"ownerBlogUrl":6,"ownerDescription":46,"updatedAt":47,"install":48,"resources":61},"01M6EGTZVC78J35KSRGMQX2FSS","ai-regression-testing",null,"AI辅助开发的回归测试策略。沙盒模式API测试，无需依赖数据库，自动化的缺陷检查工作流程，以及捕捉AI盲点的模式，其中同一模型编写和审查代码。","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faffaan-m\u002Feverything-claude-code\u002Fai-regression-testing\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32],"testing","ai regression testing","regression","自动化","2026-11-10 10:12:30",false,"---\nname: ai-regression-testing\ndescription: AI辅助开发的回归测试策略。沙盒模式API测试，无需依赖数据库，自动化的缺陷检查工作流程，以及捕捉AI盲点的模式，其中同一模型编写和审查代码。\norigin: ECC\n---\n\n# AI 回归测试\n\n专为 AI 辅助开发设计的测试模式，其中同一模型编写代码并审查代码——这会形成系统性的盲点，只有自动化测试才能发现。\n\n## 何时激活\n\n* AI 代理（Claude Code、Cursor、Codex）已修改 API 路由或后端逻辑\n* 发现并修复了一个 bug——需要防止重新引入\n* 项目具有沙盒\u002F模拟模式，可用于无需数据库的测试\n* 在代码更改后运行 `\u002Fbug-check` 或类似的审查命令\n* 存在多个代码路径（沙盒与生产环境、功能开关等）\n\n## 核心问题\n\n当 AI 编写代码然后审查其自身工作时，它会将相同的假设带入这两个步骤。这会形成一个可预测的失败模式：\n\n```\nAI 编写修复 → AI 审查修复 → AI 表示“看起来正确” → 漏洞依然存在\n```\n\n**实际示例**（在生产环境中观察到）：\n\n```\n修复 1：向 API 响应添加了 notification_settings\n  → 忘记将其添加到 SELECT 查询中\n  → AI 审核时遗漏了（相同的盲点）\n\n修复 2：将其添加到 SELECT 查询中\n  → TypeScript 构建错误（列不在生成的类型中）\n  → AI 审核了修复 1，但未发现 SELECT 问题\n\n修复 3：改为 SELECT *\n  → 修复了生产路径，忘记了沙箱路径\n  → AI 审核时再次遗漏（第 4 次出现）\n\n修复 4：测试在首次运行时立即捕获了问题 ✅\n```\n\n模式：**沙盒\u002F生产环境路径不一致**是 AI 引入的 #1 回归问题。\n\n## 沙盒模式 API 测试\n\n大多数具有 AI 友好架构的项目都有一个沙盒\u002F模拟模式。这是实现快速、无需数据库的 API 测试的关键。\n\n### 设置（Vitest + Next.js App Router）\n\n```typescript\n\u002F\u002F vitest.config.ts\nimport { defineConfig } from \"vitest\u002Fconfig\";\nimport path from \"path\";\n\nexport default defineConfig({\n  test: {\n    environment: \"node\",\n    globals: true,\n    include: [\"__tests__\u002F**\u002F*.test.ts\"],\n    setupFiles: [\"__tests__\u002Fsetup.ts\"],\n  },\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \".\"),\n    },\n  },\n});\n```\n\n```typescript\n\u002F\u002F __tests__\u002Fsetup.ts\n\u002F\u002F Force sandbox mode — no database needed\nprocess.env.SANDBOX_MODE = \"true\";\nprocess.env.NEXT_PUBLIC_SUPABASE_URL = \"\";\nprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = \"\";\n```\n\n### Next.js API 路由的测试辅助工具\n\n```typescript\n\u002F\u002F __tests__\u002Fhelpers.ts\nimport { NextRequest } from \"next\u002Fserver\";\n\nexport function createTestRequest(\n  url: string,\n  options?: {\n    method?: string;\n    body?: Record\u003Cstring, unknown>;\n    headers?: Record\u003Cstring, string>;\n    sandboxUserId?: string;\n  },\n): NextRequest {\n  const { method = \"GET\", body, headers = {}, sandboxUserId } = options || {};\n  const fullUrl = url.startsWith(\"http\") ? url : `http:\u002F\u002Flocalhost:3000${url}`;\n  const reqHeaders: Record\u003Cstring, string> = { ...headers };\n\n  if (sandboxUserId) {\n    reqHeaders[\"x-sandbox-user-id\"] = sandboxUserId;\n  }\n\n  const init: { method: string; headers: Record\u003Cstring, string>; body?: string } = {\n    method,\n    headers: reqHeaders,\n  };\n\n  if (body) {\n    init.body = JSON.stringify(body);\n    reqHeaders[\"content-type\"] = \"application\u002Fjson\";\n  }\n\n  return new NextRequest(fullUrl, init);\n}\n\nexport async function parseResponse(response: Response) {\n  const json = await response.json();\n  return { status: response.status, json };\n}\n```\n\n### 编写回归测试\n\n关键原则：**为已发现的 bug 编写测试，而不是为正常工作的代码编写测试**。\n\n```typescript\n\u002F\u002F __tests__\u002Fapi\u002Fuser\u002Fprofile.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { createTestRequest, parseResponse } from \"..\u002F..\u002Fhelpers\";\nimport { GET, PATCH } from \"@\u002Fapp\u002Fapi\u002Fuser\u002Fprofile\u002Froute\";\n\n\u002F\u002F Define the contract — what fields MUST be in the response\nconst REQUIRED_FIELDS = [\n  \"id\",\n  \"email\",\n  \"full_name\",\n  \"phone\",\n  \"role\",\n  \"created_at\",\n  \"avatar_url\",\n  \"notification_settings\",  \u002F\u002F ← Added after bug found it missing\n];\n\ndescribe(\"GET \u002Fapi\u002Fuser\u002Fprofile\", () => {\n  it(\"returns all required fields\", async () => {\n    const req = createTestRequest(\"\u002Fapi\u002Fuser\u002Fprofile\");\n    const res = await GET(req);\n    const { status, json } = await parseResponse(res);\n\n    expect(status).toBe(200);\n    for (const field of REQUIRED_FIELDS) {\n      expect(json.data).toHaveProperty(field);\n    }\n  });\n\n  \u002F\u002F Regression test — this exact bug was introduced by AI 4 times\n  it(\"notification_settings is not undefined (BUG-R1 regression)\", async () => {\n    const req = createTestRequest(\"\u002Fapi\u002Fuser\u002Fprofile\");\n    const res = await GET(req);\n    const { json } = await parseResponse(res);\n\n    expect(\"notification_settings\" in json.data).toBe(true);\n    const ns = json.data.notification_settings;\n    expect(ns === null || typeof ns === \"object\").toBe(true);\n  });\n});\n```\n\n### 测试沙盒\u002F生产环境一致性\n\n最常见的 AI 回归问题：修复了生产环境路径但忘记了沙盒路径（或反之）。\n\n```typescript\n\u002F\u002F Test that sandbox responses match the expected contract\ndescribe(\"GET \u002Fapi\u002Fuser\u002Fmessages (conversation list)\", () => {\n  it(\"includes partner_name in sandbox mode\", async () => {\n    const req = createTestRequest(\"\u002Fapi\u002Fuser\u002Fmessages\", {\n      sandboxUserId: \"user-001\",\n    });\n    const res = await GET(req);\n    const { json } = await parseResponse(res);\n\n    \u002F\u002F This caught a bug where partner_name was added\n    \u002F\u002F to production path but not sandbox path\n    if (json.data.length > 0) {\n      for (const conv of json.data) {\n        expect(\"partner_name\" in conv).toBe(true);\n      }\n    }\n  });\n});\n```\n\n## 将测试集成到 Bug 检查工作流中\n\n### 自定义命令定义\n\n```markdown\n\u003C!-- .claude\u002Fcommands\u002Fbug-check.md -->\n# Bug 检查\n\n## 步骤 1：自动化测试（强制，不可跳过）\n\n在代码审查前**首先**运行以下命令：\n\n    npm run test       # Vitest 测试套件\n    npm run build      # TypeScript 类型检查 + 构建\n\n- 如果测试失败 → 报告为最高优先级 Bug\n- 如果构建失败 → 将类型错误报告为最高优先级\n- 只有在两者都通过后，才能继续到步骤 2\n\n## 步骤 2：代码审查（AI 审查）\n\n1. 沙盒\u002F生产环境路径一致性\n2. API 响应结构是否符合前端预期\n3. SELECT 子句的完整性\n4. 包含回滚的错误处理\n5. 乐观更新的竞态条件\n\n## 步骤 3：对于每个修复的 Bug，提出回归测试方案\n```\n\n### 工作流程\n\n```\nUser: \"バグチェックして\" (or \"\u002Fbug-check\")\n  │\n  ├─ Step 1: npm run test\n  │   ├─ FAIL → 发现机械性错误（无需AI判断）\n  │   └─ PASS → 继续\n  │\n  ├─ Step 2: npm run build\n  │   ├─ FAIL → 发现类型错误\n  │   └─ PASS → 继续\n  │\n  ├─ Step 3: AI代码审查（考虑已知盲点）\n  │   └─ 报告发现的问题\n  │\n  └─ Step 4: 对每个修复编写回归测试\n      └─ 下次bug-check时捕获修复是否破坏功能\n```\n\n## 常见的 AI 回归模式\n\n### 模式 1：沙盒\u002F生产环境路径不匹配\n\n**频率**：最常见（在 4 个回归问题中观察到 3 个）\n\n```typescript\n\u002F\u002F ❌ AI adds field to production path only\nif (isSandboxMode()) {\n  return { data: { id, email, name } };  \u002F\u002F Missing new field\n}\n\u002F\u002F Production path\nreturn { data: { id, email, name, notification_settings } };\n\n\u002F\u002F ✅ Both paths must return the same shape\nif (isSandboxMode()) {\n  return { data: { id, email, name, notification_settings: null } };\n}\nreturn { data: { id, email, name, notification_settings } };\n```\n\n**用于捕获它的测试**：\n\n```typescript\nit(\"sandbox and production return same fields\", async () => {\n  \u002F\u002F In test env, sandbox mode is forced ON\n  const res = await GET(createTestRequest(\"\u002Fapi\u002Fuser\u002Fprofile\"));\n  const { json } = await parseResponse(res);\n\n  for (const field of REQUIRED_FIELDS) {\n    expect(json.data).toHaveProperty(field);\n  }\n});\n```\n\n### 模式 2：SELECT 子句遗漏\n\n**频率**：在使用 Supabase\u002FPrisma 添加新列时常见\n\n```typescript\n\u002F\u002F ❌ New column added to response but not to SELECT\nconst { data } = await supabase\n  .from(\"users\")\n  .select(\"id, email, name\")  \u002F\u002F notification_settings not here\n  .single();\n\nreturn { data: { ...data, notification_settings: data.notification_settings } };\n\u002F\u002F → notification_settings is always undefined\n\n\u002F\u002F ✅ Use SELECT * or explicitly include new columns\nconst { data } = await supabase\n  .from(\"users\")\n  .select(\"*\")\n  .single();\n```\n\n### 模式 3：错误状态泄漏\n\n**频率**：中等——当向现有组件添加错误处理时\n\n```typescript\n\u002F\u002F ❌ Error state set but old data not cleared\ncatch (err) {\n  setError(\"Failed to load\");\n  \u002F\u002F reservations still shows data from previous tab!\n}\n\n\u002F\u002F ✅ Clear related state on error\ncatch (err) {\n  setReservations([]);  \u002F\u002F Clear stale data\n  setError(\"Failed to load\");\n}\n```\n\n### 模式 4：乐观更新未正确回滚\n\n```typescript\n\u002F\u002F ❌ No rollback on failure\nconst handleRemove = async (id: string) => {\n  setItems(prev => prev.filter(i => i.id !== id));\n  await fetch(`\u002Fapi\u002Fitems\u002F${id}`, { method: \"DELETE\" });\n  \u002F\u002F If API fails, item is gone from UI but still in DB\n};\n\n\u002F\u002F ✅ Capture previous state and rollback on failure\nconst handleRemove = async (id: string) => {\n  const prevItems = [...items];\n  setItems(prev => prev.filter(i => i.id !== id));\n  try {\n    const res = await fetch(`\u002Fapi\u002Fitems\u002F${id}`, { method: \"DELETE\" });\n    if (!res.ok) throw new Error(\"API error\");\n  } catch {\n    setItems(prevItems);  \u002F\u002F Rollback\n    alert(\"削除に失敗しました\");\n  }\n};\n```\n\n## 策略：在发现 Bug 的地方进行测试\n\n不要追求 100% 的覆盖率。相反：\n\n```\n在 \u002Fapi\u002Fuser\u002Fprofile 发现 bug → 为 profile API 编写测试\n在 \u002Fapi\u002Fuser\u002Fmessages 发现 bug → 为 messages API 编写测试\n在 \u002Fapi\u002Fuser\u002Ffavorites 发现 bug → 为 favorites API 编写测试\n在 \u002Fapi\u002Fuser\u002Fnotifications 没有发现 bug → 暂时不编写测试\n```\n\n**为什么这在 AI 开发中有效：**\n\n1. AI 倾向于重复犯**同一类错误**\n2. Bug 集中在复杂区域（身份验证、多路径逻辑、状态管理）\n3. 一旦经过测试，该特定回归问题**就不会再次发生**\n4. 测试数量随着 Bug 修复而有机增长——没有浪费精力\n\n## 快速参考\n\n| AI 回归模式 | 测试策略 | 优先级 |\n|---|---|---|\n| 沙盒\u002F生产环境不匹配 | 断言沙盒模式下响应结构相同 | ? 高 |\n| SELECT 子句遗漏 | 断言响应中包含所有必需字段 | ? 高 |\n| 错误状态泄漏 | 断言出错时状态已清理 | ? 中 |\n| 缺少回滚 | 断言 API 失败时状态已恢复 | ? 中 |\n| 类型转换掩盖 null | 断言字段不为 undefined | ? 中 |\n\n## 要 \u002F 不要\n\n**要：**\n\n* 发现 bug 后立即编写测试（如果可能，在修复之前）\n* 测试 API 响应结构，而不是实现细节\n* 将运行测试作为每次 bug 检查的第一步\n* 保持测试快速（在沙盒模式下总计 \u003C 1 秒）\n* 以测试所预防的 bug 来命名测试（例如，\"BUG-R1 regression\"）\n\n**不要：**\n\n* 为从未出现过 bug 的代码编写测试\n* 相信 AI 自我审查可以作为自动化测试的替代品\n* 因为“只是模拟数据”而跳过沙盒路径测试\n* 在单元测试足够时编写集成测试\n* 追求覆盖率百分比——追求回归预防\n\n","# AI 回归测试\n\n专为 AI 辅助开发设计的测试模式，其中同一模型编写代码并审查代码——这会形成系统性的盲点，只有自动化测试才能发现。\n\n## 何时激活\n\n* AI 代理（Claude Code、Cursor、Codex）已修改 API 路由或后端逻辑\n* 发现并修复了一个 bug——需要防止重新引入\n* 项目具有沙盒\u002F模拟模式，可用于无需数据库的测试\n* 在代码更改后运行 `\u002Fbug-check` 或类似的审查命令\n* 存在多个代码路径（沙盒与生产环境、功能开关等）\n\n## 核心问题\n\n当 AI 编写代码然后审查其自身工作时，它会将相同的假设带入这两个步骤。这会形成一个可预测的失败模式：\n\n```\nAI 编写修复 → AI 审查修复 → AI 表示“看起来正确” → 漏洞依然存在\n```\n\n**实际示例**（在生产环境中观察到）：\n\n```\n修复 1：向 API 响应添加了 notification_settings\n  → 忘记将其添加到 SELECT 查询中\n  → AI 审核时遗漏了（相同的盲点）\n\n修复 2：将其添加到 SELECT 查询中\n  → TypeScript 构建错误（列不在生成的类型中）\n  → AI 审核了修复 1，但未发现 SELECT 问题\n\n修复 3：改为 SELECT *\n  → 修复了生产路径，忘记了沙箱路径\n  → AI 审核时再次遗漏（第 4 次出现）\n\n修复 4：测试在首次运行时立即捕获了问题 ✅\n```\n\n模式：**沙盒\u002F生产环境路径不一致**是 AI 引入的 #1 回归问题。\n\n## 沙盒模式 API 测试\n\n大多数具有 AI 友好架构的项目都有一个沙盒\u002F模拟模式。这是实现快速、无需数据库的 API 测试的关键。\n\n### 设置（Vitest + Next.js App Router）\n\n```typescript\n\u002F\u002F vitest.config.ts\nimport { defineConfig } from \"vitest\u002Fconfig\";\nimport path from \"path\";\n\nexport default defineConfig({\n  test: {\n    environment: \"node\",\n    globals: true,\n    include: [\"__tests__\u002F**\u002F*.test.ts\"],\n    setupFiles: [\"__tests__\u002Fsetup.ts\"],\n  },\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \".\"),\n    },\n  },\n});\n```\n\n```typescript\n\u002F\u002F __tests__\u002Fsetup.ts\n\u002F\u002F Force sandbox mode — no database needed\nprocess.env.SANDBOX_MODE = \"true\";\nprocess.env.NEXT_PUBLIC_SUPABASE_URL = \"\";\nprocess.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = \"\";\n```\n\n### Next.js API 路由的测试辅助工具\n\n```typescript\n\u002F\u002F __tests__\u002Fhelpers.ts\nimport { NextRequest } from \"next\u002Fserver\";\n\nexport function createTestRequest(\n  url: string,\n  options?: {\n    method?: string;\n    body?: Record\u003Cstring, unknown>;\n    headers?: Record\u003Cstring, string>;\n    sandboxUserId?: string;\n  },\n): NextRequest {\n  const { method = \"GET\", body, headers = {}, sandboxUserId } = options || {};\n  const fullUrl = url.startsWith(\"http\") ? url : `http:\u002F\u002Flocalhost:3000${url}`;\n  const reqHeaders: Record\u003Cstring, string> = { ...headers };\n\n  if (sandboxUserId) {\n    reqHeaders[\"x-sandbox-user-id\"] = sandboxUserId;\n  }\n\n  const init: { method: string; headers: Record\u003Cstring, string>; body?: string } = {\n    method,\n    headers: reqHeaders,\n  };\n\n  if (body) {\n    init.body = JSON.stringify(body);\n    reqHeaders[\"content-type\"] = \"application\u002Fjson\";\n  }\n\n  return new NextRequest(fullUrl, init);\n}\n\nexport async function parseResponse(response: Response) {\n  const json = await response.json();\n  return { status: response.status, json };\n}\n```\n\n### 编写回归测试\n\n关键原则：**为已发现的 bug 编写测试，而不是为正常工作的代码编写测试**。\n\n```typescript\n\u002F\u002F __tests__\u002Fapi\u002Fuser\u002Fprofile.test.ts\nimport { describe, it, expect } from \"vitest\";\nimport { createTestRequest, parseResponse } from \"..\u002F..\u002Fhelpers\";\nimport { GET, PATCH } from \"@\u002Fapp\u002Fapi\u002Fuser\u002Fprofile\u002Froute\";\n\n\u002F\u002F Define the contract — what fields MUST be in the response\nconst REQUIRED_FIELDS = [\n  \"id\",\n  \"email\",\n  \"full_name\",\n  \"phone\",\n  \"role\",\n  \"created_at\",\n  \"avatar_url\",\n  \"notification_settings\",  \u002F\u002F ← Added after bug found it missing\n];\n\ndescribe(\"GET \u002Fapi\u002Fuser\u002Fprofile\", () => {\n  it(\"returns all required fields\", async () => {\n    const req = createTestRequest(\"\u002Fapi\u002Fuser\u002Fprofile\");\n    const res = await GET(req);\n    const { status, json } = await parseResponse(res);\n\n    expect(status).toBe(200);\n    for (const field of REQUIRED_FIELDS) {\n      expect(json.data).toHaveProperty(field);\n    }\n  });\n\n  \u002F\u002F Regression test — this exact bug was introduced by AI 4 times\n  it(\"notification_settings is not undefined (BUG-R1 regression)\", async () => {\n    const req = createTestRequest(\"\u002Fapi\u002Fuser\u002Fprofile\");\n    const res = await GET(req);\n    const { json } = await parseResponse(res);\n\n    expect(\"notification_settings\" in json.data).toBe(true);\n    const ns = json.data.notification_settings;\n    expect(ns === null || typeof ns === \"object\").toBe(true);\n  });\n});\n```\n\n### 测试沙盒\u002F生产环境一致性\n\n最常见的 AI 回归问题：修复了生产环境路径但忘记了沙盒路径（或反之）。\n\n```typescript\n\u002F\u002F Test that sandbox responses match the expected contract\ndescribe(\"GET \u002Fapi\u002Fuser\u002Fmessages (conversation list)\", () => {\n  it(\"includes partner_name in sandbox mode\", async () => {\n    const req = createTestRequest(\"\u002Fapi\u002Fuser\u002Fmessages\", {\n      sandboxUserId: \"user-001\",\n    });\n    const res = await GET(req);\n    const { json } = await parseResponse(res);\n\n    \u002F\u002F This caught a bug where partner_name was added\n    \u002F\u002F to production path but not sandbox path\n    if (json.data.length > 0) {\n      for (const conv of json.data) {\n        expect(\"partner_name\" in conv).toBe(true);\n      }\n    }\n  });\n});\n```\n\n## 将测试集成到 Bug 检查工作流中\n\n### 自定义命令定义\n\n```markdown\n\u003C!-- .claude\u002Fcommands\u002Fbug-check.md -->\n# Bug 检查\n\n## 步骤 1：自动化测试（强制，不可跳过）\n\n在代码审查前**首先**运行以下命令：\n\n    npm run test       # Vitest 测试套件\n    npm run build      # TypeScript 类型检查 + 构建\n\n- 如果测试失败 → 报告为最高优先级 Bug\n- 如果构建失败 → 将类型错误报告为最高优先级\n- 只有在两者都通过后，才能继续到步骤 2\n\n## 步骤 2：代码审查（AI 审查）\n\n1. 沙盒\u002F生产环境路径一致性\n2. API 响应结构是否符合前端预期\n3. SELECT 子句的完整性\n4. 包含回滚的错误处理\n5. 乐观更新的竞态条件\n\n## 步骤 3：对于每个修复的 Bug，提出回归测试方案\n```\n\n### 工作流程\n\n```\nUser: \"バグチェックして\" (or \"\u002Fbug-check\")\n  │\n  ├─ Step 1: npm run test\n  │   ├─ FAIL → 发现机械性错误（无需AI判断）\n  │   └─ PASS → 继续\n  │\n  ├─ Step 2: npm run build\n  │   ├─ FAIL → 发现类型错误\n  │   └─ PASS → 继续\n  │\n  ├─ Step 3: AI代码审查（考虑已知盲点）\n  │   └─ 报告发现的问题\n  │\n  └─ Step 4: 对每个修复编写回归测试\n      └─ 下次bug-check时捕获修复是否破坏功能\n```\n\n## 常见的 AI 回归模式\n\n### 模式 1：沙盒\u002F生产环境路径不匹配\n\n**频率**：最常见（在 4 个回归问题中观察到 3 个）\n\n```typescript\n\u002F\u002F ❌ AI adds field to production path only\nif (isSandboxMode()) {\n  return { data: { id, email, name } };  \u002F\u002F Missing new field\n}\n\u002F\u002F Production path\nreturn { data: { id, email, name, notification_settings } };\n\n\u002F\u002F ✅ Both paths must return the same shape\nif (isSandboxMode()) {\n  return { data: { id, email, name, notification_settings: null } };\n}\nreturn { data: { id, email, name, notification_settings } };\n```\n\n**用于捕获它的测试**：\n\n```typescript\nit(\"sandbox and production return same fields\", async () => {\n  \u002F\u002F In test env, sandbox mode is forced ON\n  const res = await GET(createTestRequest(\"\u002Fapi\u002Fuser\u002Fprofile\"));\n  const { json } = await parseResponse(res);\n\n  for (const field of REQUIRED_FIELDS) {\n    expect(json.data).toHaveProperty(field);\n  }\n});\n```\n\n### 模式 2：SELECT 子句遗漏\n\n**频率**：在使用 Supabase\u002FPrisma 添加新列时常见\n\n```typescript\n\u002F\u002F ❌ New column added to response but not to SELECT\nconst { data } = await supabase\n  .from(\"users\")\n  .select(\"id, email, name\")  \u002F\u002F notification_settings not here\n  .single();\n\nreturn { data: { ...data, notification_settings: data.notification_settings } };\n\u002F\u002F → notification_settings is always undefined\n\n\u002F\u002F ✅ Use SELECT * or explicitly include new columns\nconst { data } = await supabase\n  .from(\"users\")\n  .select(\"*\")\n  .single();\n```\n\n### 模式 3：错误状态泄漏\n\n**频率**：中等——当向现有组件添加错误处理时\n\n```typescript\n\u002F\u002F ❌ Error state set but old data not cleared\ncatch (err) {\n  setError(\"Failed to load\");\n  \u002F\u002F reservations still shows data from previous tab!\n}\n\n\u002F\u002F ✅ Clear related state on error\ncatch (err) {\n  setReservations([]);  \u002F\u002F Clear stale data\n  setError(\"Failed to load\");\n}\n```\n\n### 模式 4：乐观更新未正确回滚\n\n```typescript\n\u002F\u002F ❌ No rollback on failure\nconst handleRemove = async (id: string) => {\n  setItems(prev => prev.filter(i => i.id !== id));\n  await fetch(`\u002Fapi\u002Fitems\u002F${id}`, { method: \"DELETE\" });\n  \u002F\u002F If API fails, item is gone from UI but still in DB\n};\n\n\u002F\u002F ✅ Capture previous state and rollback on failure\nconst handleRemove = async (id: string) => {\n  const prevItems = [...items];\n  setItems(prev => prev.filter(i => i.id !== id));\n  try {\n    const res = await fetch(`\u002Fapi\u002Fitems\u002F${id}`, { method: \"DELETE\" });\n    if (!res.ok) throw new Error(\"API error\");\n  } catch {\n    setItems(prevItems);  \u002F\u002F Rollback\n    alert(\"削除に失敗しました\");\n  }\n};\n```\n\n## 策略：在发现 Bug 的地方进行测试\n\n不要追求 100% 的覆盖率。相反：\n\n```\n在 \u002Fapi\u002Fuser\u002Fprofile 发现 bug → 为 profile API 编写测试\n在 \u002Fapi\u002Fuser\u002Fmessages 发现 bug → 为 messages API 编写测试\n在 \u002Fapi\u002Fuser\u002Ffavorites 发现 bug → 为 favorites API 编写测试\n在 \u002Fapi\u002Fuser\u002Fnotifications 没有发现 bug → 暂时不编写测试\n```\n\n**为什么这在 AI 开发中有效：**\n\n1. AI 倾向于重复犯**同一类错误**\n2. Bug 集中在复杂区域（身份验证、多路径逻辑、状态管理）\n3. 一旦经过测试，该特定回归问题**就不会再次发生**\n4. 测试数量随着 Bug 修复而有机增长——没有浪费精力\n\n## 快速参考\n\n| AI 回归模式 | 测试策略 | 优先级 |\n|---|---|---|\n| 沙盒\u002F生产环境不匹配 | 断言沙盒模式下响应结构相同 | ? 高 |\n| SELECT 子句遗漏 | 断言响应中包含所有必需字段 | ? 高 |\n| 错误状态泄漏 | 断言出错时状态已清理 | ? 中 |\n| 缺少回滚 | 断言 API 失败时状态已恢复 | ? 中 |\n| 类型转换掩盖 null | 断言字段不为 undefined | ? 中 |\n\n## 要 \u002F 不要\n\n**要：**\n\n* 发现 bug 后立即编写测试（如果可能，在修复之前）\n* 测试 API 响应结构，而不是实现细节\n* 将运行测试作为每次 bug 检查的第一步\n* 保持测试快速（在沙盒模式下总计 \u003C 1 秒）\n* 以测试所预防的 bug 来命名测试（例如，\"BUG-R1 regression\"）\n\n**不要：**\n\n* 为从未出现过 bug 的代码编写测试\n* 相信 AI 自我审查可以作为自动化测试的替代品\n* 因为“只是模拟数据”而跳过沙盒路径测试\n* 在单元测试足够时编写集成测试\n* 追求覆盖率百分比——追求回归预防\n\n",{"name":5,"origin":38,"description":7},"ECC","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faffaan-m\u002Feverything-claude-code\u002Fai-regression-testing",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":49,"prompt":59,"zipAvailable":60},[50,53,56],{"tool":51,"command":52},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill ai-regression-testing",{"tool":54,"command":55},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill ai-regression-testing",{"tool":57,"command":58},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill ai-regression-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faffaan-m\u002Feverything-claude-code\u002Fai-regression-testing\u002FSKILL.md",true,[62,68,74],{"path":63,"resourceType":64,"resourceTypeDesc":65,"sizeBytes":66,"sourceHtmlUrl":67},"assets\u002Fconfig.yaml","ASSET","资产文件",31,"https:\u002F\u002Fgithub.com\u002FPatkik\u002FBarcelon_store\u002Fblob\u002Fmaster\u002F.github\u002Fskills\u002Fmongoose-mongodb\u002Fassets\u002Fconfig.yaml",{"path":69,"resourceType":70,"resourceTypeDesc":71,"sizeBytes":72,"sourceHtmlUrl":73},"references\u002FGUIDE.md","REFERENCE","参考文档",25,"https:\u002F\u002Fgithub.com\u002FPatkik\u002FBarcelon_store\u002Fblob\u002Fmaster\u002F.github\u002Fskills\u002Fmongoose-mongodb\u002Freferences\u002FGUIDE.md",{"path":75,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":79},"scripts\u002Fhelper.py","SCRIPT","脚本",94,"https:\u002F\u002Fgithub.com\u002FPatkik\u002FBarcelon_store\u002Fblob\u002Fmaster\u002F.github\u002Fskills\u002Fmongoose-mongodb\u002Fscripts\u002Fhelper.py",[81,93,103,112,118,125],{"skillNo":82,"name":83,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":89,"publishedAt":33},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[86],{"categoryNo":87,"categoryName":88,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[90,91,92],"model","model hierarchy","model hierarchy skill",{"skillNo":94,"name":95,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":99,"publishedAt":33},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[98],{"categoryNo":87,"categoryName":88,"taxonomyType":21,"taxonomyTypeDesc":22},[100,101,102],"MCP","zoom","zoom mcp",{"skillNo":104,"name":105,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":108,"publishedAt":33},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[109,110,111,101],"sdk","zoom apps sdk","apps",{"skillNo":113,"name":114,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":115,"categories":116,"keywords":117,"publishedAt":33},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[114],{"skillNo":119,"name":120,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":124,"publishedAt":33},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[123],{"categoryNo":87,"categoryName":88,"taxonomyType":21,"taxonomyTypeDesc":22},[120],{"skillNo":126,"name":127,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":128,"categories":129,"keywords":130,"publishedAt":33},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[127]]