# L2 MCP 验证 PR #208 撤回流程时浮出的两个非平凡发现

> **日期**：2026-05-01
> **场景**：PR #214 跑 A bucket（L2 双语回归），用 Playwright MCP 在本地 worktree 跑撤回流程
> **类型**：architecture / gap

## 发现 1：UI 撤回走 approval 引擎，不走 form-management 撤回端点

**直觉**：PR #208 修了 `/api/v1/form-management/instances/<id>/withdraw` 的 WITHDRAWN/CANCELLED 语义统一 → L2 上点撤回应当能验证这个修复。

**实测**：approval-center 的 Withdraw 按钮调的是 `POST /api/v1/approval/<approval_instance_id>/withdraw`（approval 引擎），**不是**那个 form-management 端点。

```ts
// frontend/src/app/(modules)/approval-center/page.tsx:832
const result = await withdrawApproval(instanceId, ...);

// frontend/src/services/api/approval.ts:479
return apiClient.post(`/approval/${instanceId}/withdraw`, ...);
```

而 PR #208 修的 form-management 撤回端点是另一条路径：
```ts
// frontend/src/services/api/form-management.ts:719
return apiClient.post(`${BASE_URL}/instances/${id}/withdraw`, ...);
```

**这两条路径分工**：
- approval 引擎 withdraw：发 Temporal `withdraw` signal → workflow 调 activity → POST callback → form-management.handleApprovalCompleted（endReason=WITHDRAWN）→ FormInstance.status=WITHDRAWN（**这是 PR #208 修的点**：之前会落 CANCELLED）
- form-management.withdrawInstance：同步事务里直接落 FormInstance.WITHDRAWN，再调 approvalService.withdraw 通知 Temporal（fire-and-forget 不影响 form 端）

UI 用的是前者。**PR #208 的修复点在异步 callback 路径，不在同步 form 端路径**。L1 测试 `instance-state-machine.api.test.ts:252` 就是直接调 `approvalIntegration.handleApprovalCompleted({endReason: 'WITHDRAWN'})` 锁定的就是 callback 那条路径。

**意义**：PR #208 是有效的修复，UI 层确实会受益——只是受益方式是"approval engine workflow 推进 callback 进来时不再误落 CANCELLED"，不是"form-management 同步路径"。

## 发现 2：L2 撤回完整 DB 端到端依赖 Temporal worker（本地 worktree 默认没起）

**症状**：UI 点 Withdraw → confirm → toast "Withdrawn successfully"，列表瞬间变 Withdrawn（frontend 乐观更新）；刷新页面后状态又退回 Running；DB 查 `form_instances.status` 仍是 `PENDING_APPROVAL`、`approval_instances.status` 仍是 `RUNNING`。

**链路**：
1. UI → POST `/approval/<id>/withdraw` 200 OK
2. 后端 `temporalService.sendSignal('withdraw', workflowId)` 成功（log 里有 `Sent signal withdraw to workflow ...`）
3. **但 workflow 不会被处理**——worker 是独立进程（`backend/src/core/workflow/temporal/worker.ts` 通过 `npm run start:temporal` 启动），本地 worktree 默认只跑 `npm run start:dev`，没起 worker
4. 没有 worker 跑 workflow → activity 不被调用 → callback 不会 POST 回 form-management → DB 状态永远停在 PENDING_APPROVAL/RUNNING

**绕开方案（验证 L2 撤回）**：
- 起 worker：`cd backend && TEMPORAL_NAMESPACE=approval-form-polish npm run start:temporal`（注意 namespace 跟 .env 一致）
- 或：直接发 HTTP 给 form-management 同步端点，绕开 Temporal（仅用于不依赖 worker 的快速验证）

**证据**：approval-center 列表里历史 `_2026_0002 / _0004 / _0005` 都显示 Withdrawn——这些是某次环境完整时（worker 在跑）撤回成功落地的；本 session 撤回的 `_0008` 因为没 worker 停在乐观更新阶段。

## 发现 3：M11 字段类型前端 renderer 缺 6 种实现

打开"L2 全字段类型测试表单"，渲染时如下字段都显示"不支持的字段类型："占位：

- Rating
- Serial Number
- Address
- Country/Region
- Cascade Select
- Signature
- Rich Text

后端 schema 完全无障碍（JSONB 能存任何结构，L1 `data-integrity.api.test.ts` 覆盖了 array/object 落库）。**这是前端 form renderer 的实现 gap**——可能是 v4/M11 设计时声明了所有类型，但实现上只接了一半。

需要进 todos 里跟踪。

## 应用范围

- L2 验证撤回链路前先看 worker 是否在跑；只测 UI smoke 的话不需要，但要测"DB 端到端"必须先起 worker
- 对 PR #208 后续测试的 L2 期望要现实：5 审批动作（通过/拒绝/转交/退回/加签）全部依赖 worker，本地没 worker 时只能验"按钮存在 + 调用 API 成功"，状态机推进无法验
- M11 字段 gap 需要前端层面排查——后端契约是干净的，bug 在 renderer 实现里
