|
| 1 | +# Handoff: Card v2 Image Block 测试迁移完成 |
| 2 | + |
| 3 | +## 状态 |
| 4 | + |
| 5 | +**测试进度**: 829/829 全部通过 ✅ |
| 6 | + |
| 7 | +**分支**: `card-template-v2` |
| 8 | + |
| 9 | +**完成日期**: 2026-04-04 |
| 10 | + |
| 11 | +## 核心变更 |
| 12 | + |
| 13 | +card-draft-controller 从 markdown 字符串格式迁移到 JSON `CardBlock[]` 格式。 |
| 14 | + |
| 15 | +### 格式对比 |
| 16 | + |
| 17 | +**旧格式 (markdown)** |
| 18 | +``` |
| 19 | +> 思考中... |
| 20 | +
|
| 21 | +> 工具调用结果... |
| 22 | +
|
| 23 | +最终答案文本 |
| 24 | +``` |
| 25 | + |
| 26 | +**新格式 (JSON CardBlock[])** |
| 27 | +```json |
| 28 | +[ |
| 29 | + {"type": 1, "markdown": "思考中..."}, |
| 30 | + {"type": 2, "markdown": "工具调用结果..."}, |
| 31 | + {"type": 0, "markdown": "最终答案文本"}, |
| 32 | + {"type": 3, "mediaId": "xxx"} |
| 33 | +] |
| 34 | +``` |
| 35 | + |
| 36 | +### CardBlock 类型定义 |
| 37 | + |
| 38 | +```typescript |
| 39 | +type CardBlock = |
| 40 | + | { type: 0; markdown: string } // answer |
| 41 | + | { type: 1; markdown: string } // thinking |
| 42 | + | { type: 2; markdown: string } // tool |
| 43 | + | { type: 3; mediaId: string }; // image |
| 44 | +``` |
| 45 | + |
| 46 | +## 修复的 3 个测试失败 |
| 47 | + |
| 48 | +### 1. card-service.test.ts > createAICard returns card instance |
| 49 | + |
| 50 | +**问题**: 测试期望 `cardParamMap` 不包含 `hasQuote`/`quoteContent`,但实际代码添加了这些字段。 |
| 51 | + |
| 52 | +**修复**: 更新测试断言以包含新字段: |
| 53 | +```typescript |
| 54 | +expect(body.cardData?.cardParamMap).toEqual({ |
| 55 | + config: '{"autoLayout":true,"enableForward":true}', |
| 56 | + content: '', |
| 57 | + stop_action: 'true', |
| 58 | + hasQuote: 'false', |
| 59 | + quoteContent: '', |
| 60 | +}); |
| 61 | +``` |
| 62 | + |
| 63 | +### 2. inbound-handler.test.ts > finalizes card with default content when no textual output is produced |
| 64 | + |
| 65 | +**问题**: 测试期望 `"✅ Done"`,但得到 `"[]"` (空 JSON 数组)。 |
| 66 | + |
| 67 | +**根本原因**: `getRenderedContent()` 返回 JSON 字符串。当没有内容时,返回 `"[]"` 而不是空字符串。由于 `"[]"` 是 truthy,`||` fallback 不触发。 |
| 68 | + |
| 69 | +**修复**: 在 `card-draft-controller.ts` 的 `getRenderedContent` 中检查空数组: |
| 70 | +```typescript |
| 71 | +getRenderedContent: (options?) => { |
| 72 | + const blocks = renderTimelineAsBlocks(options); |
| 73 | + if (blocks.length === 0) { |
| 74 | + return ""; |
| 75 | + } |
| 76 | + return JSON.stringify(blocks); |
| 77 | +}, |
| 78 | +``` |
| 79 | + |
| 80 | +### 3. inbound-handler.test.ts > card mode + media |
| 81 | + |
| 82 | +**问题**: 测试期望 `sendMessage` 被调用(旧行为),但新行为使用 `uploadMedia → appendImageBlock`。 |
| 83 | + |
| 84 | +**根本原因**: |
| 85 | +1. `send-service` mock 缺少 `uploadMedia` 导出 |
| 86 | +2. 测试期望基于旧的 markdown fallback 行为 |
| 87 | + |
| 88 | +**修复**: |
| 89 | +1. 添加 `uploadMediaMock` 到 shared mocks |
| 90 | +2. 更新 `send-service` mock 导出 `uploadMedia` |
| 91 | +3. 更新测试预期匹配新行为: |
| 92 | +```typescript |
| 93 | +// 旧: expect(sendMessageMock).toHaveBeenCalledWith(...) |
| 94 | +// 新: |
| 95 | +expect(shared.uploadMediaMock).toHaveBeenCalledWith( |
| 96 | + expect.objectContaining({ dmPolicy: "open", messageType: "card" }), |
| 97 | + "https://cdn.example.com/report.pdf", |
| 98 | + "image", |
| 99 | + undefined, |
| 100 | +); |
| 101 | +expect(shared.finishAICardMock).toHaveBeenCalledWith( |
| 102 | + card, |
| 103 | + JSON.stringify([ |
| 104 | + { type: 3, mediaId: "media_img_123" }, |
| 105 | + { type: 0, markdown: "final output" }, |
| 106 | + ]), |
| 107 | + undefined, |
| 108 | + { quotedRef: {...} }, |
| 109 | +); |
| 110 | +``` |
| 111 | + |
| 112 | +## 测试迁移辅助函数 |
| 113 | + |
| 114 | +在 `card-draft-controller.test.ts` 中添加: |
| 115 | + |
| 116 | +```typescript |
| 117 | +function parseBlocks(content: string): CardBlock[] { |
| 118 | + try { |
| 119 | + return JSON.parse(content); |
| 120 | + } catch { |
| 121 | + return []; |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +function getBlockText(blocks: CardBlock[], index: number): string { |
| 126 | + const block = blocks[index]; |
| 127 | + if (!block) return ""; |
| 128 | + return "markdown" in block ? block.markdown : ""; |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +## 关键文件变更 |
| 133 | + |
| 134 | +| 文件 | 变更类型 | |
| 135 | +|------|----------| |
| 136 | +| `src/card-draft-controller.ts` | `getRenderedContent` 返回 JSON,空数组返回 `""` | |
| 137 | +| `src/reply-strategy-card.ts` | mediaUrls → `uploadMedia` → `appendImageBlock` | |
| 138 | +| `tests/unit/card-service.test.ts` | 添加 `hasQuote`/`quoteContent` 断言 | |
| 139 | +| `tests/unit/inbound-handler.test.ts` | 添加 `uploadMediaMock`,更新 media 测试预期 | |
| 140 | +| `tests/unit/card-draft-controller.test.ts` | 添加 `parseBlocks`/`getBlockText` 辅助函数 | |
| 141 | + |
| 142 | +## 下一步 |
| 143 | + |
| 144 | +- [ ] 真机验证 image block 渲染 |
| 145 | +- [ ] 确认 mediaId 上传后图片正常显示 |
| 146 | +- [ ] 合并到主分支 |
0 commit comments