# Sprint 2 任务拆解(W2-3 · 18 人天 · 58 子任务) > **主任务**:T-A-14 ~ T-A-22, T-B-01 ~ T-B-05 > **目标**:Vitest/Playwright 骨架 + OpenAPI Codegen + 后端 5 端点对接 > **闸门**:G1(OpenAPI 类型生成无 `any`) ## 0. Sprint 2 概览 | 任务 | 主任务 | 估时 | 子任务数 | |---|---|---|---| | T-A-14 | Vitest + RTL + MSW 配置 | 0.5d | 4 | | T-A-15 | 示范单测:ModeIndicator | 0.25d | 3 | | T-A-16 | 示范单测:SafeMarkdown | 0.25d | 3 | | T-A-17 | 示范单测:FormCard | 0.5d | 4 | | T-A-18 | Playwright 配置 | 0.5d | 3 | | T-A-19 | 示范 e2e:theme.spec | 0.5d | 3 | | T-A-20 | 示范 e2e:navigation.spec | 0.5d | 3 | | T-A-21 | OpenAPI Codegen 接入 | 1.0d | 5 | | T-A-22 | 示范调用:mock server | 0.5d | 4 | | T-B-01 | `POST /api/chat/send` | 1.5d | 6 | | T-B-02 | `GET /api/chat/history` | 0.5d | 4 | | T-B-03 | 3 个 consultations 端点 | 1.0d | 5 | | T-B-04 | 4 个 plans 端点 | 1.0d | 5 | | T-B-05 | assessment + audit/upload | 2.0d | 6 | | **合计** | **15 任务** | **10.5d + 缓冲 7.5d = 18d** | **58** | --- ## T-A-14 · Vitest + RTL + MSW 配置(0.5d · 4 子任务) ### ST-S2-A-14.1 写 `vitest.config.ts`(0.125d) - **产出**:`apps/web/vitest.config.ts`(jsdom env + setup 文件 + alias) ### ST-S2-A-14.2 写 `setup.ts`(0.125d) - **产出**:`@testing-library/jest-dom/vitest` + MSW server 启动/关闭 ### ST-S2-A-14.3 写 `renderWithProviders.tsx`(0.125d) - **产出**:`packages/test-utils/renderWithProviders.tsx`(QueryClient + Theme + Router + Intl) - **验收**: - [ ] `pnpm --filter web test` 跑通 ### ST-S2-A-14.4 占位测试验证(0.125d) - **产出**:`Math.test.ts`(1+1=2) - **验收**: - [ ] 占位测试 1 绿 --- ## T-A-15 · 示范单测:ModeIndicator(0.25d · 3 子任务) ### ST-S2-A-15.1 写 4 mode 渲染测试(0.125d) - **覆盖**: - explore: "探索模式" - generating: "生成中" - auditing: "审核中" - adjusting: "调整中" - **验收**: - [ ] 4 模式 label 正确 ### ST-S2-A-15.2 写 `deriveMode` 决策树测试(0.0625d → 0d) - **覆盖**: - isAuditActive → auditing - currentPlan + no audit → adjusting - province + score → generating - 其他 → explore - **验收**: - [ ] 100% branch coverage ### ST-S2-A-15.3 验证 100% 覆盖(0.0625d → 0d) - **命令**:`pnpm test --coverage` - **验收**: - [ ] `ModeIndicator.tsx` line + branch 各 100% --- ## T-A-16 · 示范单测:SafeMarkdown(0.25d · 3 子任务) ### ST-S2-A-16.1 写 XSS 注入测试(0.125d) - **覆盖**: - `` → 渲染为文本 - `` → onerror 剥 - `` → href sanitize - **验收**: - [ ] 3 注入用例全过 ### ST-S2-A-16.2 写正常 markdown 渲染(0.0625d → 0d) - **覆盖**:h1/ul/code/blockquote 渲染正确 ### ST-S2-A-16.3 验证(0.0625d → 0d) --- ## T-A-17 · 示范单测:FormCard(0.5d · 4 子任务) ### ST-S2-A-17.1 字段 touched 显示验证(0.125d) - **验收**: - [ ] 字段未 touch 时不显示错误 - [ ] touch 后显示错误 ### ST-S2-A-17.2 跳步守卫测试(0.125d) - **验收**: - [ ] basic 未完成时点击 subjects 步骤 → 阻止 + 提示 ### ST-S2-A-17.3 subjects 数组拼装测试(0.125d) - **验收**: - [ ] 选物理/化学/生物 → 提交时 `subjects: ["physics", "chemistry", "biology"]` ### ST-S2-A-17.4 验证 80% 覆盖(0.125d) - **命令**:`pnpm test --coverage --testPathPattern=FormCard` --- ## T-A-18 · Playwright 配置(0.5d · 3 子任务) ### ST-S2-A-18.1 写 `playwright.config.ts`(0.25d) - **产出**:`apps/web/e2e/playwright.config.ts` - chromium + webkit - viewports: 375 (mobile) / 768 (tablet) / 1280 (desktop) - baseURL: http://localhost:3000 - **验收**: - [ ] `pnpm --filter web e2e` 启动浏览器 ### ST-S2-A-18.2 写 `smoke.spec.ts`(0.125d) - **验收**: - [ ] 访问 `/` 看到 Sidebar ### ST-S2-A-18.3 写 webServer 配置(0.125d) - **产出**:自动启 `pnpm dev` --- ## T-A-19 · 示范 e2e:theme.spec(0.5d · 3 子任务) ### ST-S2-A-19.1 写 light/dark/system 切换(0.25d) - **覆盖**: - 切到 dark → `data-theme="dark"` 出现 - 切到 system → `data-theme` 移除 - **验收**: - [ ] 3 场景全绿 ### ST-S2-A-19.2 写持久化测试(0.125d) - **覆盖**: - 刷新页面后 localStorage 保留选择 - **验收**: - [ ] `expect(localStorage.getItem('theme')).toBe('dark')` ### ST-S2-A-19.3 写闪白防护测试(0.125d) - **覆盖**: - `page.waitForLoadState('domcontentloaded')` 在 100ms 内 - **验收**: - [ ] DOMContentLoaded → theme 已应用 --- ## T-A-20 · 示范 e2e:navigation.spec(0.5d · 3 子任务) ### ST-S2-A-20.1 写桌面导航(0.125d) - **覆盖**:viewport 1280 → Sidebar 可见,MobileNav 隐藏 ### ST-S2-A-20.2 写移动导航(0.125d) - **覆盖**:viewport 375 → MobileNav 可见,Sidebar 隐藏 ### ST-S2-A-20.3 写跳转测试(0.25d) - **覆盖**: - Sidebar /plans → 跳转 - MobileNav /plans → 跳转 - **验收**: - [ ] 4 场景全绿 --- ## T-A-21 · OpenAPI Codegen 接入(1.0d · 5 子任务) ### ST-S2-A-21.1 启动后端 OpenAPI(0.125d) - **命令**:`curl http://localhost:8000/openapi.json > openapi.json` - **验收**: - [ ] 看到 ~30+ 端点(V2 增至 30+) ### ST-S2-A-21.2 写 `codegen.config.ts`(0.25d) - **产出**:`packages/api-client/codegen.config.ts` - schema: openapi.json - generator: typescript-fetch - output: src/gen ### ST-S2-A-21.3 写 `turbo.json` generate 任务(0.125d) - **产出**:`generate-api-client` 任务依赖 `^build-admin` - **验收**: - [ ] `pnpm turbo run generate-api-client` 跑通 ### ST-S2-A-21.4 跑 codegen 看产物(0.25d) - **验收**: - [ ] `packages/api-client/src/gen/services/` 30+ 服务类 - [ ] 0 个 `any`(**G1 闸门**) ### ST-S2-A-21.5 写 `packages/api-client/src/index.ts` barrel(0.25d) - **验收**: - [ ] `import { AuthService } from '@gaokao/api-client'` 可用 --- ## T-A-22 · 示范调用:mock server(0.5d · 4 子任务) ### ST-S2-A-22.1 写 `apps/web/lib/api/chat.ts`(0.125d) - **API**: ```ts export const apiClient = { chat: { send: (consultationId: string | undefined, body: { message: string }) => Promise, getHistory: (consultationId: string) => Promise<{ messages: Message[] }>, } }; ``` ### ST-S2-A-22.2 写 MSW handler(0.125d) - **产出**:`apps/web/mocks/handlers/chat.ts` 拦截 `/api/chat/send` ### ST-S2-A-22.3 在 `page.tsx` 临时接 apiClient(0.125d) - **验收**: - [ ] 类型管道通(**不替换 useChat**,仅验证) ### ST-S2-A-22.4 写 Zod 校验 match 字段(0.125d) - **约束**:§0.1 R-NEW-3(`z.number().int().min(1).max(5)`) - **验收**: - [ ] match=6 → Zod 抛错 --- ## T-B-01 · `POST /api/chat/send`(1.5d · 6 子任务) ### ST-S2-B-01.1 对接 OpenAPI 类型(0.25d) - **产出**:`import type { ChatSendRequest, ChatResponse } from '@gaokao/api-client/gen'` ### ST-S2-B-01.2 写 `useChatStore.sendMessage`(0.5d) - **产出**:`apps/web/stores/chatStore.ts` - **API**:`sendMessage(consultationId, body)` → 调用 `apiClient.chat.send` - **验收**: - [ ] 401/403 错误码正确捕获 ### ST-S2-B-01.3 写错误码 E03001/03002/03003 处理(0.25d) - **验收**: - [ ] 3 错误码 → 对应 Toast 提示 ### ST-S2-B-01.4 写单测(0.25d) - **覆盖**:成功 / 401 / 403 / 500 ### ST-S2-B-01.5 写 e2e 草稿(0.125d) - **产出**:`chat-send.spec.ts`(Sprint 3 完成) ### ST-S2-B-01.6 写 Storybook 故事(0.125d) - **产出**:`ChatSendButton.stories.ts` --- ## T-B-02 · `GET /api/chat/history`(0.5d · 4 子任务) ### ST-S2-B-02.1 对接 OpenAPI(0.125d) ### ST-S2-B-02.2 写 useQuery(0.125d) - **验收**: - [ ] cursor 分页支持 ### ST-S2-B-02.3 写 auth 校验(0.125d) - **验收**: - [ ] consultation_id 无访问令牌 → 401 ### ST-S2-B-02.4 写单测(0.125d) --- ## T-B-03 · 3 个 consultations 端点(1.0d · 5 子任务) ### ST-S2-B-03.1 列表(0.25d) - **出参**:`{consultations: [{id, title, updated_at, profile, has_plan, has_audit}]}` ### ST-S2-B-03.2 详情(0.25d) - **出参**:完整 messages + profile + plan + audit ### ST-S2-B-03.3 DELETE 软删除(0.25d) - **验收**: - [ ] 数据保留 30 天 ### ST-S2-B-03.4 写 useConsultation hook(0.125d) ### ST-S2-B-03.5 写单测(0.125d) --- ## T-B-04 · 4 个 plans 端点(1.0d · 5 子任务) ### ST-S2-B-04.1 列表(0.25d) - **出参**:`{plans: [{id, name, created_at, profile, distribution: {rush,stable,safe}}]}` ### ST-S2-B-04.2 详情(0.125d) ### ST-S2-B-04.3 PATCH name 修改(0.25d) - **验收**: - [ ] 仅支持 name,其他字段 PATCH 拒绝 ### ST-S2-B-04.4 DELETE 软删除(0.25d) ### ST-S2-B-04.5 写单测(0.125d) --- ## T-B-05 · assessment + audit/upload(2.0d · 6 子任务) ### ST-S2-B-05.1 写 `useAssessment` hook(0.25d) - **API**:`POST /api/assessment`(Holland RIAS) ### ST-S2-B-05.2 写 `useAudit.upload` multipart(0.5d) - **验收**: - [ ] Excel 5MB / Image 10MB / PDF 10MB 限制 - [ ] MIME 校验 ### ST-S2-B-05.3 写上传进度条(0.25d) - **API**:`XMLHttpRequest` + `progress` 事件 ### ST-S2-B-05.4 写 AI 处理进度(0.5d) - **API**:轮询 `/api/audit/{id}/status` ### ST-S2-B-05.5 错误码 E04001 处理(0.25d) - **验收**: - [ ] 上游 AI 失败 → Toast 重试 ### ST-S2-B-05.6 写单测(0.25d) --- ## Sprint 2 收口验收 - [ ] 15 主任务 / 58 子任务全部完成 - [ ] **G1 通过**:OpenAPI 生成 0 个 `any` - [ ] 测试覆盖率:ModeIndicator/SafeMarkdown/FormCard 100% / 100% / 80%+ - [ ] E2E:theme.spec / navigation.spec 全绿 - [ ] 进入 Sprint 3 前 commit:``