任务 0:执行
> cd forge-server && mvn -pl forge-framework/forge-plugin-parent/forge-plugin-generator test -DskipITs
> cd forge-admin-ui && pnpm test --run
> ```
> 确认基线绿灯,写入 §4。
## 2. 覆盖范围
### 子迭代 A — 工作台
#### P0 — 后端 WorkspaceService
| 方法 | 场景 | 输入 | Mock 行为 | 预期结果 |
|------|------|------|-----------|---------|
| summary | 普通用户 | userId=1 | 各 mapper 返回固定值 | VO.todoCount=3, doneWeekCount=5, startedRunningCount=2, ccUnreadCount=1 |
| summary | 空数据 | userId=999 | mapper 返回 0 | VO 全 0 |
| todoCount | 普通用户 | userId=1 | mapper count=3 | 3 |
| todoCount | 缓存 | 短时间多次调用 | — | 第二次走缓存(如设计了缓存) |
#### P0 — 前端 workspace 路由 + 顶部导航
| 场景 | 步骤 | 预期 |
|------|------|------|
| 路由兼容 | 访问 `/flow/todo` | 仍可正常加载 |
| 工作台默认页 | 访问 `/workspace` | 重定向到 `/workspace/summary` |
| 顶部导航三项 | superadmin 登录 | 看到三个按钮 |
| 顶部导航两项 | 普通用户登录 | 不显示"能力中心" |
| 徽标更新 | mock todo-count=5 | 顶部徽标显示 5 |
| 徽标轮询 | mock 30s 后 todo-count=10 | 徽标更新为 10 |
#### P1 — 应用中心提交 toast
| 场景 | 步骤 | 预期 |
|------|------|------|
| 提交成功触发 toast | mock AiCrudPage 提交成功事件 | toast 出现 + 含"查看流转"按钮 |
| 提交失败不显示 | mock 提交失败 | toast 不出现 |
| 点击 toast 跳转 | 点击"查看流转" | router 跳 `/workspace/started` |
#### P1 — 工作台默认页用户偏好
| 场景 | 步骤 | 预期 |
|------|------|------|
| 默认偏好 summary | mock 偏好接口返回 summary | `/workspace` 重定向到 `/workspace/summary` |
| 偏好 todo | mock 偏好返回 todo | `/workspace` 重定向到 `/workspace/todo` |
| 偏好接口异常 | mock 偏好接口 500 | 回退 summary,不白屏 |
| 修改偏好 | PUT 偏好 = todo | 下次进入重定向到 todo |
### 子迭代 B — 节点就地配置
#### P0 — 后端节点配置 API
| 方法 | 场景 | 输入 | Mock 行为 | 预期结果 |
|------|------|------|-----------|---------|
| getNodeConfig | 正常 | modelKey + nodeId | 节点存在 | VO 含审批人 / 表单 / 字段权限 / updateTime |
| getNodeConfig | 节点不存在 | 错误 nodeId | mapper 返回 null | 404 |
| saveNodeConfig | 并发冲突 | expectedUpdateTime=旧值 | 实际 updateTime 已变化 | 409 Conflict |
| saveNodeConfig | 正常保存 | expectedUpdateTime=当前值 | — | 200 + 新 VO,updateTime 已递增 |
| listInlineNodes | 仅返回人工节点 | modelKey=sample_purchase_order | 含 userTask + serviceTask 节点 | 只返回 userTask |
| listInlineNodes | configured 判断 | 节点配置完整 vs 缺审批人 | — | configured 字段对应 true/false |
#### P0 — 前端 FlowNodeInlineConfig 组件
| 场景 | 步骤 | 预期 |
|------|------|------|
| 抽屉打开取配置 | mount + show=true | 调用 GET 节点配置接口 |
| 保存成功 | 编辑后点保存 | 调用 PUT + 触发 save 事件 |
| 409 冲突弹确认 | mock PUT 返回 409 | 弹出确认框"是否覆盖" |
| 覆盖后重试 | 确认覆盖 | 重新 PUT(用新 updateTime) |
| 自动节点只读 | nodeType=serviceTask | 抽屉显示只读说明 + 链接到完整设计器 |
#### P1 — 步骤条 + 应用中心嵌入
| 场景 | 步骤 | 预期 |
|------|------|------|
| 步骤条加载 | mount with modelKey | 调用 inline-nodes 接口 |
| 长流程滚动 | mock 25 节点 | 容器内滚动而非页面滚动 |
| 配置完成度显示 | 节点 configured=true | 绿色对勾 |
| 配置缺失提示 | configured=false | 灰色感叹号 + 悬浮提示具体缺失 |
| 选中节点弹抽屉 | 点击步骤 | drawerShow=true + 节点 ID 透传 |
| 应用中心打开完整设计器 | 点击"高级编辑" | router 跳 `/flow/design` + source=appCenter |
| 步骤条展示网关节点 | mock 含 exclusiveGateway 节点 | 菱形节点出现,标记 inlineEditable |
| tab 视觉骨架 | 进入业务流程 tab | 顶部 4 步骤进度可见 |
#### P0 — 网关业务化就地配置(B8)
| 方法 | 场景 | 输入 | Mock 行为 | 预期结果 |
|------|------|------|-----------|---------|
| getConfig | 简单网关 | exclusiveGateway 含 3 个出向条件 | — | VO.inlineEditable=true,outgoingConditions 长度=3 |
| getConfig | 复杂网关 | 含复合表达式 | — | VO.inlineEditable=false + reason 说明 |
| saveConfig | 同意预设 | preset=approve | — | BPMN `<conditionExpression>` = `${approvalResult == 'approve'}` |
| saveConfig | 驳回预设 | preset=reject | — | BPMN 表达式 = `${approvalResult == 'reject'}` |
| saveConfig | 自定义条件 | sourceField=amount, op='>', value=10000 | — | BPMN 表达式 = `${amount > 10000}` |
| saveConfig | 并发冲突 | expectedUpdateTime=旧值 | — | 409 Conflict |
| convertToBpmnExpression | 双向转换语义等价 | DTO → BPMN → DTO | — | 来回转换不丢失语义 |
| listPresets | 4 个预设 | — | — | 同意 / 驳回 / 退回 / 终止 都返回 |
| isInlineEditable | 分支 > 5 | 6 个出向条件 | — | false + reason="分支数超过 5" |
| isInlineEditable | 含函数调用 | 表达式 `${calc(a)}` | — | false + reason="表达式含函数调用" |
#### P1 — 网关就地配置 UI
| 场景 | 步骤 | 预期 |
|------|------|------|
| 简单网关可编辑 | mock inlineEditable=true | 抽屉显示业务化条件表单 |
| 复杂网关不可编辑 | inlineEditable=false | 抽屉显示禁用提示 + 跳完整设计器按钮 |
| 应用快捷预设 | 点击"同意路由" | 出向条件填充 approvalResult == 'approve' → 下一节点 |
| BPMN 表达式高级编辑 | 展开"高级设置" | 显示 raw 表达式列表,可手写覆盖 |
### 子迭代 C — 渲染引擎收敛
#### P0 — schema 适配层 formCreateToAiSchema
| 输入控件 | form-create JSON | 预期 AiForm field |
|---|---|---|
| input | `{type:'input', field:'name', title:'姓名'}` | `{code:'name', label:'姓名', type:'string'}` |
| number | `{type:'inputNumber', field:'age'}` | `{code:'age', type:'number'}` |
| select | `{type:'select', options:[{label,value}]}` | `{type:'select', options}` |
| radio | `{type:'radio', options}` | `{type:'radio', options}` |
| checkbox | `{type:'checkbox'}` | `{type:'checkboxGroup'}` |
| date | `{type:'datePicker'}` | `{type:'date'}` |
| datetime | `{type:'datePicker', props:{type:'datetime'}}` | `{type:'datetime'}` |
| switch | `{type:'switch'}` | `{type:'switch'}` |
| upload | `{type:'upload'}` | `{type:'upload'}` |
| 未知 | `{type:'customSignature'}` | unknownFields 数组含此项 + 主 schema 占位 |
| 嵌套 layout | grid 内有 2 个 input | 扁平化为 2 个 field,丢弃 layout(或附 `meta.layout`) |
| validate | `{validate:[{required:true}]}` | `{required: true}` |
| 必填 + max | `{validate:[{required:true, max:50}]}` | `{required:true, maxLength:50}` |
#### P0 — 节点动态表单切换 AiForm
| 场景 | 步骤 | 预期 |
|------|------|------|
| 默认走 AiForm | feature flag = ai | todo.vue 渲染 AiForm + 适配 schema |
| 降级走 form-create | feature flag = formCreate | 渲染 FlowFormCreateRenderer |
| 字段权限三态 (AiForm) | visible/editable/required | 三态正确呈现 |
| 字段权限三态 (form-create) | 同上 | 行为等价 |
#### P0 — schema 迁移工具
| 方法 | 场景 | 输入 | Mock 行为 | 预期结果 |
|------|------|------|-----------|---------|
| migrateModel | 全部成功 | modelKey=sample | 3 节点都有 form_json | report.success=3, failed=0 |
| migrateModel | 部分失败 | — | 1 节点含未知组件 | report.success=2, failed=1, failedNodeIds 含该节点 |
| migrateModel | 幂等 | 已迁移过的 modelKey | — | 跳过已迁移节点,success=0 |
| migrateModel | dry-run | dryRun=true | — | 不写库,log 表插入 dry_run=true 记录 |
| migrateModel | 批次大小 | batchSize=2 | 5 节点 | 分 3 批处理,每批最大 2 节点 |
| migrateAll | 多模型 | — | 5 个模型 | 累计 report |
#### P0 — C7 form-create 清理前置检查
| 方法 | 场景 | Mock 行为 | 预期结果 |
|------|------|-----------|---------|
| FormCreatePurgePrecheckJob.check | 三项全绿 | 迁移 100% + flag 24h + 指标正常 | canPurge=true, blockReasons=[] |
| FormCreatePurgePrecheckJob.check | 迁移未完成 | 还有 form_json 未迁移 | canPurge=false, blockReasons 含 "migration not fully completed" |
| FormCreatePurgePrecheckJob.check | flag 不足 24h | flag 开启 1h | canPurge=false, blockReasons 含 "feature flag not stable" |
| FormCreatePurgePrecheckJob.check | 错误率超阈值 | mock 错误率 = 1% | canPurge=false, blockReasons 含 "error rate exceeds threshold" |
#### P0 — C7 回滚预案演练
| 方法 | 场景 | 预期 |
|------|------|------|
| FormSchemaRebuildFromFormJsonJob.rebuild | 测试环境单模型 | 从 form_json 重建 form_schema,与 C4 迁移结果 schema 等价 |
| FormSchemaRebuildFromFormJsonJob.rebuildAll | 全量重建 | 所有模型重建成功,无字段丢失 |
#### P1 — 通用流程表单设计器迁移
| 场景 | 步骤 | 预期 |
|------|------|------|
| /flow/form 加载 | 进入页面 | 显示 AiForm 设计器(与业务对象设计器一致) |
| /flow/template 保存 | 编辑 + 保存 | schema 落入 form_schema 列 |
#### P1 — 视觉回归
| 场景 | 步骤 | 预期 |
|------|------|------|
| 采购单待办 vs 业务运行时 | playwright 截图 | 像素差 < 1% |
| 独立流程待办 vs 业务运行时 | playwright 截图 | 像素差 < 1% |
| 字段权限灰显视觉 | mock editable=false | 灰显样式一致 |
### 不测试(明确列出原因)
- BPMN 引擎自身:本变更不修改 Flowable。
- 工作台首页"我的草稿"占位:本轮未实现,下一轮覆盖。
- 复杂网关(事件网关 / 并行网关编辑):本轮 B8 仅 exclusiveGateway / inclusiveGateway 可编辑。
- 移动端 / App 端:本变更聚焦 Web 后台。
- form-create 卸载后的依赖深度审计:C7 完成后 pnpm/npm 包是否还有间接引用,由依赖分析工具兜底,不纳入单测。
## 3. 执行计划
- [ ] **Step 0**:跑基线
cd forge-server && mvn -pl forge-framework/forge-plugin-parent/forge-plugin-generator test -DskipITs
cd forge-admin-ui && pnpm test --run
```