# Meeting Attendance Outlook Sync E2E Report (2026-03-01)

## 范围
- 模块：meeting-attendance / outlook sync
- 类型：E2E（MCP Playwright 手工执行）
- 目标：验证权限修复后的同步链路与异常处理

## 执行环境
- Frontend: http://localhost:3000
- Backend: http://localhost:3001/api/v1
- 页面：`/meetingattendance/integrations/outlook`

## 用例与结果
1. 候选会议加载（到达断言 + 稳定断言）
- 步骤：打开 Outlook Sync 页面，等待候选会议表格渲染
- 断言：页面可见 `Outlook Sync` 标题、候选会议表格有数据行
- 结果：通过

2. 手动纳管会议（到达断言 + 成功断言）
- 步骤：点击候选会议某条 `Manage`
- 断言：该行从 `Manage` 变为 `Managed`，出现来源覆盖下拉和 `Save`
- 结果：通过

3. 触发对账（修复验证）
- 步骤：点击 `Run Reconcile`
- 断言：接口 `POST /meeting-attendance/integrations/outlook/sync/reconcile` 返回 200，响应 `{"accepted":true,...}`
- 结果：通过
- 说明：已修复 Graph delta 调用方式，改用 `Prefer: odata.maxpagesize`，不再使用 `$top`

4. 候选会议接口异常可读性（回归验证）
- 步骤：模拟异常场景触发候选接口失败
- 断言：页面展示具体错误文本，不出现前端 Runtime AxiosError 崩溃
- 结果：通过

5. 候选会议筛选与批量纳管（新增能力验证）
- 步骤：
  - 默认加载候选会议（不勾选“包含已取消/包含历史”）
  - 勾选“包含历史会议”后查看列表
  - 点击“全选未纳管”->“批量纳管”
- 断言：
  - 默认过滤生效（历史/取消项默认不展示）
  - 勾选后可见历史候选项
  - 批量纳管成功提示 `Batch managed: 2`，对应行状态更新为 `Managed`
- 结果：通过

6. 同步任务观测信息（新增能力验证）
- 步骤：
  - 打开 Outlook Sync 页面
  - 观察源邮箱表格新增列（最近同步、最近对账、最近错误）
  - 触发一次对账后刷新页面
- 断言：
  - 表格可见 `Last Sync / Last Reconcile / Last Error`（中英文环境对应文案）
  - 最近同步与最近对账时间可展示，错误为空时显示 `-`
- 结果：通过

7. 邮箱级重试入口（新增能力验证）
- 步骤：
  - 在源邮箱行点击 `Reconcile now`（立即对账）
  - 观察该行 `Last Sync / Last Reconcile` 时间变化
- 断言：
  - 操作可成功返回
  - 对应邮箱行最近同步、最近对账时间刷新
- 结果：通过

8. 已纳管会议维护区（新增能力验证）
- 步骤：
  - 打开 Outlook Sync 页面已纳管会议区域
  - 使用关键词搜索、点击刷新、检查分页控件状态
- 断言：
  - 已纳管会议列表正常展示（标题/开始时间/类型/状态/最近同步）
  - 搜索接口可用（`GET /integrations/outlook/bindings`）
  - 分页信息显示 `Page x/y`
- 结果：通过

## 本次修复点
- 前端：候选会议加载增加错误兜底，页面内展示错误信息
- 前端：候选会议新增默认过滤与筛选控件（关键词/包含已取消/包含历史/仅未纳管）
- 前端：候选会议新增批量纳管（全选未纳管、清空选择、批量提交）
- 前端：源邮箱列表新增同步观测列（最近同步/最近对账/最近错误）
- 前端：源邮箱行新增“立即对账”按钮（邮箱级重试入口）
- 前端：新增“已纳管会议”列表区（关键词检索 + 分页）
- 后端：
  - 候选会议查询错误映射为可读业务错误
  - 对账 delta 异常映射为可读业务错误
  - delta 查询从 `$top` 改为 `Prefer: odata.maxpagesize`
  - 源邮箱查询返回同步观测字段（cursor 时间戳 + subscription 最近错误）
  - 新增已纳管绑定查询接口（邮箱维度、关键词、分页）

## 结论
- Outlook 同步核心链路（候选拉取 -> 手动纳管 -> 触发对账）可用
- 当前可继续进入下一步增强（批量纳管、筛选、观测告警）

## 追加回归（2026-03-01 18:30 PST）
9. 候选刷新 + 已纳管搜索（500 回归）
- 步骤：
  - 打开 `meetingattendance/integrations/outlook`
  - 点击候选区 `Refresh`
  - 在已纳管区输入 `FF Workspace` 并点击 `Search`
- 断言：
  - 页面无前端 Runtime AxiosError
  - 控制台 `error` 级别消息为 0
  - 网络请求 `GET /integrations/outlook/candidates`、`GET /integrations/outlook/bindings` 返回 200
- 结果：通过

## 追加回归（2026-03-02 02:35 PST）
10. 已纳管会议详情弹窗（新增能力验证）
- 步骤：
  - 打开 `meetingattendance/integrations/outlook`
  - 在已纳管列表点击 `Details`
  - 查看详情字段并执行一次来源覆盖保存
- 断言：
  - 详情弹窗展示绑定核心字段（Graph Event ID/iCalUId/类型/状态/最近同步/主来源/生效来源/本地会议与系列 ID）
  - 弹窗内可保存会议级来源覆盖（调用 `PATCH /integrations/outlook/bindings/:id/source`）
  - 控制台 `error/warning` 级别消息为 0
- 结果：通过

11. 绑定同步历史时间线（新增能力验证）
- 步骤：
  - 在已纳管列表打开 `Details`
  - 检查“同步历史”表格
  - 再执行一次来源覆盖保存并刷新历史
- 断言：
  - 请求 `GET /integrations/outlook/bindings/:id/history` 返回 200
  - 历史表格包含时间/事件/结果/消息字段
  - 来源覆盖后新增 `SOURCE_OVERRIDE_UPDATED` 事件
- 结果：通过

12. 同步历史“仅失败事件”过滤（新增能力验证）
- 步骤：
  - 打开已纳管详情弹窗
  - 勾选“仅失败事件”
- 断言：
  - 页面仅展示 `resultStatus=ERROR` 的历史记录
  - 无失败记录时显示 `No data`
- 结果：通过

13. 失败日志结构化展示（新增能力验证）
- 步骤：
  - 打开已纳管详情弹窗同步历史
  - 查看失败事件记录的“错误码/消息”
- 断言：
  - 失败记录可展示 `errorCode/statusCode`
  - 消息前缀包含处理阶段（如 `APPLY_UPDATED_EVENT`）
- 结果：通过

14. 历史筛选与导出（新增能力验证）
- 步骤：
  - 在详情弹窗同步历史区选择事件类型/阶段过滤
  - 点击 `Export CSV`
- 断言：
  - 过滤结果与选择条件一致
  - 导出的 CSV 内容与当前过滤结果一致（字段含 time/eventType/resultStatus/errorCode/stage/message）
- 结果：通过

## 追加回归（2026-03-02 03:10 PST）
15. 历史导出空集分支与有数据分支（回归补充）
- 步骤：
  - 在详情弹窗中设置筛选为“仅失败事件 + SOURCE_OVERRIDE_UPDATED + 无阶段”，点击 `Export CSV`
  - 取消“仅失败事件”后再次点击 `Export CSV`
- 断言：
  - 空结果导出时弹窗提示 `No data`，页面无崩溃
  - 有结果导出时浏览器成功下载 CSV 文件（`/tmp/playwright-mcp-output/.../*.csv`）
  - 控制台 `error` 级别消息为 0
- 结果：通过

## 追加回归（2026-03-02 03:40 PST）
16. 同步历史服务端分页与过滤（新增能力验证）
- 步骤：
  - 打开 `Details` 弹窗，观察历史请求
  - 勾选“仅失败事件”
  - 点击导出 CSV
- 断言：
  - 历史请求走服务端分页：`GET .../history?page=1&pageSize=20`
  - 失败过滤走服务端参数：`GET .../history?...&onlyError=true`
  - 导出走后端导出接口：`GET .../history/export.csv?onlyError=true`
  - 上述请求均返回 200
- 结果：通过

17. 候选系列会议可见性（seriesMaster）现状确认
- 步骤：
  - 打开候选会议列表
  - 勾选“包含历史会议”
- 断言：
  - 当前测试邮箱数据集中，候选列表展示为 `occurrence`，未出现 `seriesMaster`
  - 该结果为当前数据现状，不代表代码过滤了 `seriesMaster`
- 结果：通过（数据现状记录）

18. 候选系列会议可见性修复验证（seriesMaster）
- 步骤：
  - 打开 Outlook Sync 页面候选会议区
  - 观察候选类型下拉与列表
  - 在候选类型下拉中选择 `seriesMaster`
- 断言：
  - 候选列表可出现 `seriesMaster` 类型会议（同邮箱下系列主会议）
  - 类型下拉可用，选择 `seriesMaster` 后仅展示系列主会议
- 结果：通过

## 追加验证（2026-03-02 13:30 PST）
19. 系列纳管 + 单次 occurrence 排除（代码级回归）
- 覆盖范围：
  - 后端：系列归一纳管（occurrence/exception -> seriesMaster）、自动实例覆盖、排除/取消排除 API
  - 前端：候选列表“系列纳管”标识、排除此实例按钮、详情排除列表与取消排除
- 验证命令：
  - `cd backend && npm run prisma:generate`
  - `cd backend && npm run build`
  - `cd frontend && npm run build`
- 断言：
  - 后端编译通过，新增接口与数据模型类型可编译
  - 前端编译通过，Outlook 同步页无类型错误
- 结果：通过

20. E2E 执行说明
- 当前轮次未追加新的 MCP 场景脚本验证（本次以编译回归为主）。
- 后续建议：补 1 轮 MCP 交互验证（系列纳管后 occurrence 自动受管、排除后不再同步、取消排除后可恢复）。

## 追加回归（2026-03-02 04:20 PST）
21. MCP 冒烟：系列候选与纳管链路
- 步骤：
  - 打开 `/meetingattendance/integrations/outlook`
  - 检查候选类型下拉是否包含 `seriesMaster`
  - 点击一条 `occurrence` 的 `Manage`
- 断言：
  - 候选区可见 `seriesMaster` 类型
  - 首次点击返回 500（原因：本地库未执行新增迁移）
  - 执行 `cd backend && npm run prisma:migrate:deploy` 后再次点击，`POST /integrations/outlook/bindings` 返回 `201`
- 结果：通过（迁移后）

22. MCP 冒烟：系列纳管后 occurrence 展示策略
- 步骤：
  - 在候选列表点击一条 occurrence 的 `Manage`
  - 观察候选列表更新后的动作列
- 断言：
  - seriesMaster 仍保持已纳管
  - 其下 occurrence 行展示 `Series managed` 与 `Exclude occurrence` 按钮
  - 已纳管列表出现该系列下多个 occurrence 条目（服务端分页）
- 结果：通过

23. API 全链路回归（登录态 token + 真实后端）
- 范围：源邮箱、设置、候选、纳管列表、详情、历史、导出、对账、排除实例、取消排除
- 断言：
  - `GET /mailboxes` 200
  - `GET /settings` 200, `PATCH /settings` 200
  - `GET /candidates` 200
  - `GET /bindings` 200
  - `GET /bindings/:id` 200
  - `GET /bindings/:id/history` 200
  - `GET /bindings/:id/history/export.csv` 200
  - `POST /sync/reconcile` 200
  - `POST /bindings/:id/exclusions` 201
  - `GET /bindings/:id/exclusions` 200
  - `POST /exclusions/:id/remove` 200
- 结果：通过

24. 排除实例一致性回归（接口级）
- 步骤：
  - 选择一条 `managedBySeries=true` 的 occurrence
  - 执行排除
  - 再次查询候选列表
  - 执行取消排除并再次查询
- 断言：
  - 排除后 `isExcludedBySeries=true`
  - 取消排除后 `isExcludedBySeries=false`
- 结果：通过
