# HANDOFF — UI自动化测试交接文档

> **生成时间**: 2026-08-19
> **当前分支**: `platform-auto-test`
> **最近提交**: `ac823858` feat(executor): 系统配置切换被测系统后用例执行自动跟随新地址
> **状态**: 🟢 **会话29：修复「执行中无法取消」（MySQL 1205 行锁）+ SUT URL 校验加固，已实现并部署 5.60 生产——`run_execution` 置 running 后立即 commit 释放 InnoDB 行锁（取消接口由 ~50s 1205 超时降至 91ms 返回）；实机发现收尾状态判定命中 SQLAlchemy 身份映射（expire_on_commit=False 实例不刷新）导致 cancelled 被覆盖为 completed，改为列查询 + FOR UPDATE 修复；新增 `_validate_sut_url` 拒绝非法地址（如 192.168.5.4202）。后端 186 测试全过、实机取消演练通过、恢复用例跑通 100%**

---

## 📊 当前状态（会话 26，2026-08-19）

**复杂用例通用执行机制（语义化用例执行）阶段 1-6 全部代码完成并提交**（`7cf4f03f`，参考 `_PRD_复杂用例通用执行机制_语义化用例执行.md`）。

### 范式转变：从"记录选择器"到"记录业务意图"

复杂用例（如「新建会议」21 步）此前靠 `element_mapping_service` 30+ 条模糊评分补丁逐条堆叠（会话 9-20），每次新用例都要"实测 DOM → 加补丁 → 部署验证"。新机制改为：

```
业务意图(semantic) + 执行期现场解析 + 上下文变量 + 状态验证 + 数据自建/自清理
```

| 阶段 | 内容 | 状态 |
|------|------|------|
| 1 | StepDefinition 新增 `semantic`/`verify`/`run_on`；`ExecutionContext` + `{__CTX:key__}` 渲染 | ✅ |
| 2 | 页面注册表（url_patterns + fingerprint）、`recognize_current_page` | ✅ |
| 3 | `semantic_resolver` 解析链：lookup_by_key → 模式选择器 → 关键词规则 → Claude仲裁；debug 只读接口 | ✅ |
| 4 | verify 状态验证（防假通过）+ api_call + finally/on_failure 数据自清理 | ✅ |
| 5 | 映射表 v2 显式字典（`lookup_by_key` 纯查找），`USE_FUZZY_MAPPING` 默认关停 | ✅ |
| 6 | 前端 semantic 编辑（CaseStepEditor）+ 语义化重写「新建会议」示范用例 | ✅ |

### 关键产出

- **示范用例** `case_f4359c64389f4c56a5e53fbea9f60b5e`「新建会议-语义化重写示范（semantic-demo）」
  - 17 步 = 11 semantic（input/row_checkbox/tab/icon/button）+ 4 verify + 1 finally api_call 清理
  - 动态数据（会议名/参会人）全走 `{__CTX:...__}` 上下文引用，创建/断言/清理同名
  - 11 条语义链全部 `lookup_by_key`/模式选择器确定性解析，**零 `USE_FUZZY_MAPPING` 依赖**
  - 重建脚本（幂等，先删后建）：`python backend/scripts/create_semantic_meeting_case.py`
- **修复两个参数持久化 bug**：`case_service.create()` 与 `routers.cases.py _case_to_response()` 缺 `parameters` 透传
- **映射别名**：`创建会议-查看详情`（`lookup_by_key('create_meeting','查看详情')` 原 MISS，legacy 模糊路径不回归）
- **验证**：后端 pytest 121 passed（8 个 `--deselect` 排除预存坏的 test_api.py）；前端 `npm run build` 通过

### 🎯 真实验证通过（会话 22，2026-08-18）

**验收标准"连跑两次无残留会议"达成**。验证版用例「新建会议-语义化验证版（临时-无finally）」（16 步 = `build_steps()` 弹出 finally 清理步骤）真实执行：

| 执行 | 结果 | 用时 | 步骤详情 | 残留 |
|------|------|------|----------|------|
| 第 4 轮 `exec_1ff18a20` | **passed=1, failed=0, skipped=0** | 92.0s | 步骤 4-16 全绿（1-3 URL 直达跳过），含步骤15 取消会议 / 步骤16 确认取消 | 无 |
| 第 5 轮 `exec_2eb7c625` | **passed=1, failed=0, skipped=0** | 90.8s | 同上全绿 | 无 |

- 验证脚本：`backend/scripts/create_and_run_semantic_verify.py`（幂等先删后建 + 触发执行）
- 每轮结束后核验 5.44 会议列表「共 0 条 / 暂无数据」——创建并取消的会议无残留，北京展厅会议室已释放
- 回归测试：`test_page_url_service.py` 新增 3 条（详情页→meeting_detail、新建页→create_meeting 不误判、列表页→None），全部通过

**步骤 15「取消会议」"选择器不能为空"根因**（第 3 轮失败 → 第 4/5 轮通过）：
1. `create_meeting` 万能 url_pattern `meetingV3` 与详情页 `meetingV3.*Detail` 双命中（`meetingDetail` 含**大写** Detail）
2. → URL 双候选 → fingerprint 判存 → 详情页实测 **0 个 `.room`**、0 个 `.topic_item input` → `recognize_current_page` 返回 None
3. → scope 软更新失败 → `_resolve_semantic` 返回 None → "选择器不能为空"
4. **修复**（提交 `345572f2`）：`create_meeting` url_patterns 收紧为 `CreateMeeting/createMeeting`；`meeting_detail` fingerprint 改 `.detail_top`（详情页 1 / 新建页 0）

> ⚠️ 注意：会话 22 验证的是**剔除 finally 的 16 步主流程**（本地）。finally api_call 端点仍是 PRD 占位。

### 🎯 5.60 生产部署 + 网页执行验证（会话 23，2026-08-18）

**语义化机制 + 步骤15 修复已部署 5.60 生产，并通过 5.60 网页连跑两次全绿、零残留。**

**部署内容**（SFTP 上传 → `docker compose restart app` → healthy）：
- 后端 37 个差异文件（含 `semantic_resolver.py` / `ui_context.py` / `routers/debug.py` / `page_url_mapping.json` 2.0 / `schemas/test_case.py` / `playwright_executor.py` / `element_mapping_service.py` 等）
- 前端 dist 88 个文件（含 semantic 编辑 UI）
- 部署前 5.60 仅 Phase 1-4 代码；部署脚本：`backend/_deploy_560_semantic.py`（可复用）

**激活验证**：`/api/debug/resolve` 路由 ✅、StepDefinition 含 `semantic/verify/run_on` ✅、容器 Playwright + chromium-1091 ✅、TARGET_URL 默认 5.44 ✅

**5.60 网页执行结果**（用例管理页勾选「新建会议-语义化验证版」→ 执行选中）：

| 执行 | ID | 结果 | 用时 | 步骤 15 取消会议 | 5.44 残留 |
|------|-----|------|------|------------------|----------|
| 第 1 次 | `exec_7681d4f7` | **passed=1, failed=0** | 107.5s | ✅ | 无 |
| 第 2 次 | `exec_d691cbd1` | **passed=1, failed=0** | 106.9s | ✅ | 无 |

- 16 步全绿（步骤 1-3 URL 直达跳过），含步骤 15 点击【取消会议】/ 步骤 16 确认取消
- 两次执行后核验 5.44 会议列表均「共 0 条 / 暂无数据」——**验收标准"连跑两次无残留会议"在 5.60 生产达成**
- 5.60 验证版用例：`case_9d684da4454a473892c54503e172b123`

**结论：步骤 15「选择器不能为空」修复在生产环境确认有效。**

### 🎯 finally 端点真实化 + 完整 17 步验收通过（会话 24，2026-08-18）

**完整 17 步示范用例（含 finally 自清理）在 5.60 生产验收通过。**

**根因**：原 finally 步骤 `POST /api/meeting/query` 是 PRD 占位——5.44 meetingV3 列表实时刷新走 MQTT，但**首屏列表走 HTTP 接口 `GET /meetingV3/api/message/getMessagePageList`**（浏览器抓包确认；带 `authorization` 头即可通过，无需 x-sign 签名头）。

**修复（3 处后端改动 + 1 处缺陷修复）**：
1. **`api_test_executor.py`**：AssertionEngine 新增 `body_not_contains` 断言——原始响应体不应包含指定字符串（"无残留"校验；expected 为空视为失败防假通过）
2. **`playwright_executor.py` `_do_api_call`**：新增 `_inject_session_auth`——api_call 目标为被测系统且未显式指定鉴权头时，自动从浏览器会话 `localStorage.meet_token` 注入 authorization（finally 复用主流程登录态，无需手动维护会过期的 token）
3. **`create_semantic_meeting_case.py`**：finally 步骤换真实端点，body（GET→query params）带 `companyNumber/queryType/pageSize/search={__CTX:meetingName__}`，断言 `body_not_contains` 会议名
4. **finally 步骤双跑缺陷**：主流程循环原未跳过 `run_on:finally/on_failure`，导致 finally 步骤主循环+后置块执行两次 → 加守卫推迟到后置块统一执行（步骤 17 单次）

**5.60 执行结果**（API 触发：`POST /api/executions` 创建 → `/run` 同步执行 → `GET .../results/{case_id}` 拉步骤）：

| 执行 | ID | 结果 | 用时 | 步骤 17 finally | 5.44 残留 |
|------|-----|------|------|-----------------|----------|
| 修复前（双跑） | `exec_431e2e23` | passed | 96.7s | ✅ 出现两次但均通过 | 无 |
| 修复后 | `exec_a8dbb9c4` | **passed** | 96.1s | ✅ **单次执行** | 无 |

- 16 步主流程 + finally 全绿；步骤 1-3 URL 直达跳过
- finally 断言 `body_not_contains` 校验 5.44 接口响应不含该会议名 → 通过
- 执行后核验 5.44 会议列表「暂无数据 / 共 0 条」——**零残留**

**结论：完整 17 步示范用例（含 finally 数据自清理）5.60 生产验收通过，阶段 6 最终验收完成。**

### ⚠️ 已知限制（非阻塞）

- `body_not_contains` 断言类型未同步进前端 CaseStepEditor 的 ASSERT_TYPE_OPTIONS（用例由脚本创建、前端仅展示），后续前端补充该选项

### 🔍 新建会议用例「智能定位通过但执行 step 11 失败」根因定位（会话 25，2026-08-19）

**现象**：`case_13650e0406e64779b66e6fa5de35c24a`（新建会议 18 步）智能定位验证 17/18 通过后，执行仍连续 3 次失败（exec_2ca4e6c8 / exec_c9da4587 / exec_ed5f84b6）在 **step 11 会议室勾选**：`.el-table__row:has-text("北京展厅会议室") .el-checkbox` 5s 超时，重试 2 次仍失败，单次执行 254s。

**根因链**（3 层，均实测确认）：
1. **执行期「页面直达」跳过中间导航**：登录后识别到 `create_meeting` 目标页，直接 `goto` 到 `...#/meetingV3?...CreateMeeting`，step 7/8/9 标记 skipped（页面直达优化，`playwright_executor.py` Phase 4）
2. **直达后表单 SPA 懒加载 ~10-17s**（手动点击链仅 ~2s）：goto 后 3s 表单为空、13s 才完整；step 10 填会议名称靠 Playwright `fill` 自动等待元素出现而侥幸通过
3. **step 11 checkbox 位于 Element UI 隐藏选择列**：`<td class="el-table-column--selection is-hidden">` 内 checkbox 不可见，`page.click`(5s) 与 `wait_for_selector(state=visible)`(8s) 均超时（`page.wait_for_selector(':has-text(...)')` 17.4s 成功但 checkbox 仍不可点）

**关键矛盾**：智能定位走**手动点击链**进入页面（表格 2s 加载 + checkbox 可见）验证通过；执行走**页面直达**路径（表格 13s+ + checkbox 隐藏）。两条路径 DOM 状态不同 → 智能定位结果无法覆盖执行路径。

**修复方案（已确认，暂缓执行）**：禁用 `create_meeting` 页面直达（`page_url_mapping.json` 中 url 置空 + 删 match_rules/skip_steps，**保留** url_patterns/fingerprint 供 scope 识别）→ 用例走完整点击链。详见 `Docs/PRD/问题处理/会议管理/_问题处理_新建会议用例执行失败_页面直达后会议室表格checkbox不可见.md`。

### ✅ 新建会议用例 step 11/17 执行失败修复 + 生产验收通过（会话 26，2026-08-19）

**P0 已闭环**：会话 25 的「暂缓修复」方案（禁用页面直达走点击链）被**否决**——实测手动路径在会议室被占用时同样无该行可选、且点击链更脆弱。最终方案**保留页面直达** + 增强执行器 + 修复用例 step 12，本地与生产各连跑 3 次全绿、零残留。

**新增根因（会话 26 实测确认，补全会话 25 根因链）**：

| 缺陷 | 根因 | 修复 |
|------|------|------|
| step 11 勾选超时 | ① 会议室表格懒加载 12s-130s+（原以为 10-17s）② **表格仅列出空闲会议室，被占用即从列表消失** ③ checkbox `is-hidden` 隐藏列 | 策略5：行等待改轮询（150s）+ checkbox 循环 force 勾选 + `is-checked` 校验防假通过 |
| step 17 结束会议失败 | step 12 切「预定会议」→ 会议「未开始」→ 详情页**无「结束会议」按钮**（预定会议到点也不自动开始） | step 12 改为「保持立即开始」→ 会议「进行中」→ 详情页有结束会议 |
| step 12 新选择器零匹配 | `.time_btn > div:first-child .btns` 要求 first-child 的 `.btns` 子孙，但 first-child 自身就是 `.btns`（无子孙）→ 零匹配 | 修正为 `.time_btn > div:first-child`（直接命中「立即开始」tab） |

**验收结果**（脚本：`run_case_verify.py` / `check_residue.py`）：

| 环境 | 连跑 | 结果 | 残留 |
|------|------|------|------|
| 本地（SQLite） | 3 次 | **18/18 全绿**（step 7/8/9 SKIP 页面直达预期） | 0 |
| 生产 5.60 | 3 次 | **MySQL `case_results` 三条 passed**（exec_c8c35813 / exec_6bdb170c / exec_c0f5fffc） | 容器内检查 0 |

- 部署方式：scp 覆盖执行器 + `fix_step12.py` 更新生产用例 + `docker restart plat-auto-test-app`（脚本 `deploy_fix_560.py` 一键完成）
- 回归：`test_smart_locate_semantic.py` 47/47 通过（`test_api.py` 8 个 fixture 报错为既有环境问题，与本次修复无关）
- **会议生命周期结论（长期有效）**：立即开始→进行中→详情有「结束会议」；预定会议→未开始→详情只有「修改/取消/提前开始」，无「结束会议」

### ✅ UI自动化定时任务模块 + 系统配置被测系统URL（会话 27，2026-08-19）

**按 PRD 工作流交付**：需求文档 + 计划执行文档 → 实现 → 本地全链路验证通过。PRD：`Docs/PRD/需求文档/定时任务/_PRD_需求文档_UI自动化定时任务模块与系统配置被测系统URL.md` 及 `_计划执行.md`。

**需求**：① 定时任务模块——设置周期自动执行所选模块的 UI 用例，执行完成自动生成 HTML 报告到报告中心（报告明确执行了哪些模块/用例、耗时）；② 系统管理→系统配置增加「被测系统设置」——填写系统访问 URL，保存后 UI 自动化使用该地址。

**后端实现**：

| 文件 | 说明 |
|------|------|
| `models/scheduled_task.py` | 定时任务表：`module_ids`(JSON) / `schedule_type`(interval/daily/weekly) / `interval_value` / `run_time` / `weekdays` / `auto_report` / `enabled` / `last_run_at` / `next_run_at` / `run_count` |
| `services/scheduler_service.py` | **调度引擎**：lifespan 启动 asyncio 后台循环，30s 轮询到期任务 → `asyncio.create_task` 异步触发；`compute_next_run` 支持间隔/每天/每周；**冲突跳过**（已有执行在跑则跳过本次不产生失败记录）+ `_running_tasks` 防重入；复用 `ExecutionService.create_execution(trigger_type="scheduled", trigger_by=任务名)` + `run_execution`，**不新增执行路径**；执行成功且 `auto_report` 时自动生成报告 |
| `routers/scheduled_tasks.py` | `/api/scheduled-tasks` CRUD + 启停 + 立即执行 + 运行记录（查 `trigger_type=scheduled` 执行） |
| `services/report_service.py` | 报告**按模块分组**渲染 + 头部展示被测系统 URL + 模块汇总卡片（模块名/用例数/通过率） |
| `routers/system.py` | `GET/PUT /api/system/sut-config` 被测系统 URL 读取/保存（校验 http/https 前缀；持久化到 `dingtalk_configs.target_url` 单行表，**无需 DB 迁移**） |
| `main.py` | lifespan：**启动时从 DB 加载 SUT URL 到 `settings.TARGET_URL`**（修复重启回退 bug）+ 启动/关闭调度引擎 |

**前端实现**：`views/ScheduledTasks.vue`（新建弹窗支持按间隔/每天/每周 + 模块多选显示用例数 + 运行记录抽屉 + 立即执行/启停/删除）；`api/scheduledTasks.ts`；`App.vue` 菜单新增「定时任务」+ `router` 新路由；`views/system/Settings.vue` 新增「被测系统配置」卡片（URL 校验/保存/重置 + db/env 来源标记）；`views/Reports.vue` 报告列表新增「触发方式」列（定时/手动）。

**验证结果（全部通过）**：
- 后端 `pytest` **179 passed**（补装缺失的 `pytest-asyncio`——`pytest.ini` 有 `asyncio_mode=auto` 但 requirements 未含该插件）；前端 `npm run build`（vue-tsc + vite）通过
- **真实端到端**：创建「每 1 分钟」任务 → 调度引擎到点自动触发 → 真实 Playwright 执行登录模块 5 条用例（3 过 2 败，495s）→ 任务 `run_count` 自增、`next_run_at` 推进 → 自动生成 **按模块分组、含被测系统 URL 与每步耗时** 的 HTML 报告到报告中心（`trigger_type=scheduled`）
- **SUT URL**：非法地址被拒、保存生效、重启后从 DB 加载（`source: db`）
- **浏览器冒烟**：定时任务页/新建弹窗/模块用例数/创建/启停/删除、系统配置卡片、报告「触发方式」列均正常

**关键经验/踩坑**：
- `ModuleResponse` 经 Pydantic camelCase 序列化 → 前端字段是 `caseCount`（不是 `case_count`），`vue-tsc` 类型报错也指向 `caseCount`
- 前端 vite 代理指向 **8003**（多窗口并行开发端口），改后端后需同时刷新对应端口进程
- git-bash 的 `/tmp` 与 Windows Python 的 `/tmp` 指向不同路径（`C:\tmp` vs Git 安装目录）——跨工具传 JSON 文件务必用显式绝对路径
- 多窗口并行时注意与另一后端共用 SQLite，避免并发写锁

**遗留**：验证用定时执行记录 + 报告留在开发库（`verify-interval-1min-20260819-140054`，可作演示数据）；**已部署 5.60 生产**（2026-08-19：13 后端文件 + 前端 dist 90 文件上传 → 容器重启 healthy；调度引擎启动、`/api/scheduled-tasks` 与 `/api/system/sut-config` 端点正常、定时任务菜单/页面/系统配置被测系统卡片在 5.60 网页验证通过；5.60 被测系统 URL 当前为 env 默认 `https://192.168.5.44`，在系统配置页保存一次即持久化到 MySQL）。

### ✅ 系统配置切换被测系统后用例自动跟随（方案A·执行期地址重写）（会话 28，2026-08-19）

**需求**：在「系统配置」修改被测系统 URL 后，用例执行自动使用修改后的地址——存量与新建用例均无需改动。

**背景**：切换被测系统前已核实，平台 **289 条用例**（247 UI + 42 安全），其中 **130 条 UI 用例**在 navigate 步骤硬编码 `https://192.168.5.44`（131 个绝对导航步骤，0 相对），而 `_do_navigate` 原样 `goto` 不重写主机 → 仅改配置无法让这些用例跟随。

**方案对比**：

| | A. 执行期地址重写（**采用**） | B. 批量迁移用例数据 |
|------|------|------|
| 改动 | 执行器一处代码 | 改 130 条用例数据（本地 + 生产库） |
| 数据 | **不动** | 需备份、有写坏风险 |
| 新录用例 | 自动兼容 | 录制仍写绝对地址则还会复发 |
| 回滚 | 删除重写调用即可 | 需恢复数据库 |

**实现**（`backend/app/executors/playwright_executor.py`）：
- 新增 `KNOWN_SUT_HOSTS` 类常量（从默认 `https://192.168.5.44` 派生，可扩展追加）
- 新增 `_rebase_sut_url()`：目标 URL 主机属于已知被测系统主机 → 替换为当前配置 `self.base_url` 主机（保留 scheme/路径/查询/hash）；相对路径、外部链接不受影响
- `_do_navigate` 导航前调用 → **130 条存量硬编码导航步骤全部自动跟随**
- `_do_api_call` 接口步骤 URL 走同一重写 → 鉴权注入（`_inject_session_auth`）与实际请求同步跟随

**验证**：`TestRebaseSutUrl` 3 条回归测试（切换跟随/路径hash保留/未切换不误改/外链相对地址不动）+ 后端全量 **182 passed**。

**部署**：5.60 生产（upload 1 个后端文件 `executors/playwright_executor.py` → 容器重启 healthy → 端点/日志验证通过）。

**效果**：系统配置保存新 SUT → 后续用例执行（手动/定时）的导航与接口步骤自动打到新地址。

---

### ✅ 修复「执行中无法取消」（MySQL 1205 行锁）+ SUT URL 校验加固（会话 29，2026-08-19）

**问题**：
- 用户反馈：执行中心有执行任务一直「运行中」且**无法取消**；此前一段时期用例执行也瘫痪
- **双根因**：(1) 系统配置 SUT URL 配错 `https://192.168.5.4202`（IP 段越界）→ 执行器 `Cannot navigate to invalid URL`，全部 UI 用例执行瘫痪；(2) 取消接口被 MySQL InnoDB 行锁阻塞

**根因 2 详解（取消 1205 阻塞）**：
- `run_execution` 置 `status=running` 后只 `flush` 未 `commit` → 整个 Playwright 执行期间（数十秒）持有 `executions` 行锁（InnoDB 排他锁）
- 取消接口的 `UPDATE executions` 被同一行锁阻塞 → `Lock wait timeout exceeded (1205)`（约 50s 超时），执行中永远无法取消

**修复**（`backend/app/services/execution_service.py`，三处）：

| # | 修复点 | 说明 |
|---|--------|------|
| 1 | 置 running 后**立即 commit** | 释放行锁，取消接口随时可写；commit 后实例过期，重新查询刷新 |
| 2 | 结果写入循环加**取消检测** | 执行期间已取消 → 用例结果保持 skipped，不写真实结果 |
| 3 | 收尾状态判定改**列查询 + FOR UPDATE** | ⚠️ 实机发现的隐藏 bug（见下） |

**实机发现的隐藏 bug（SQLAlchemy 身份映射）**：
- 收尾原用完整实体查询 `select(Execution)` 判定是否已取消 → 命中 SQLAlchemy **身份映射**，返回内存中过期的 execution 实例（`expire_on_commit=False` 下 commit 后实例不刷新），`.status` 读到旧值 `running`
- `FOR UPDATE` 虽执行了 SQL 但结果未灌入对象 → 已取消的执行被覆盖回 `completed`
- 修复：改为 `select(Execution.status)` **列查询（标量）+ FOR UPDATE**，绕过身份映射；回归测试 `test_cancelled_preserved_despite_stale_identity_map`（fake DB 模拟身份映射返回过期实体）

**SUT URL 校验加固**（防止再次配错导致全部用例瘫痪）：
- 后端 `routers/system.py` 新增 `_validate_sut_url()`：http/https + 主机 + 端口 1-65535 + IP 段 0-255（拦 `192.168.5.4202`）+ 域名标签合法性
- 前端 `Settings.vue` 同步表单校验（`new URL` + 端口/IP 段检查）

**验证**：
- 后端全量 **186 passed**（新增 `tests/test_sut_url_validation.py` 8 条 + `tests/test_execution_cancel.py` 4 条含身份映射回归）；前端 `npm run build` 通过
- **已部署 5.60 生产**（`routers/system.py` + `services/execution_service.py` + 前端 dist 90 文件 → 容器重启 healthy）
- 实机验证：
  - `PUT /api/system/sut-config` 非法 URL `192.168.5.4202` → **400 拒绝**；恢复 `https://192.168.5.44` → 200 持久化
  - **实机取消演练**：创建执行 → 运行 → 取消 → 取消接口 **91ms 返回**（修复前 ~50s 1205 超时），最终状态稳定 **`cancelled`**（未被覆盖为 completed）
  - 恢复用例「会议模板页面访问验证」完整跑通：**通过 1/1，100%，23.4s**（验证原有用例正常执行）

---

## 📊 会话历史时间线

| 会话 | 日期 | 主题 | 结果 |
|------|------|------|------|
| 29 | 08-19 | **修复「执行中无法取消」（MySQL 1205 行锁）+ SUT URL 校验加固**：置 running 后立即 commit 释放行锁；实机发现身份映射过期实例覆盖 cancelled 为 completed（改列查询 + FOR UPDATE）；`_validate_sut_url` 拒绝非法地址（192.168.5.4202）；结果保持 skipped | ✅ 后端 186 测试全过、已部署 5.60、实机取消演练通过（91ms + cancelled 保持）、恢复用例跑通 100% |
| 28 | 08-19 | **系统配置切换被测系统后用例自动跟随（方案A·执行期地址重写）**：`_rebase_sut_url` 导航/接口主机重写（已知 SUT 主机→当前配置地址），130 条存量硬编码用例自动跟随、不动数据 | ✅ 后端 182 测试全过、已部署 5.60 生产 |
| 27 | 08-19 | **UI自动化定时任务模块 + 系统配置被测系统URL**：调度引擎（30s 轮询 + 到期异步触发 + 冲突跳过）+ 定时任务 CRUD/启停/立即执行/运行记录 + 报告按模块分组并展示被测系统 URL + SUT URL 保存/重启持久化 + 前端定时任务页与系统配置卡片 | ✅ 按 PRD 工作流实现，后端 179 测试全过、前端 build 通过、真实端到端验收通过（已部署生产） |
| 26 | 08-19 | **新建会议用例 step 11/17 执行失败修复**：保留页面直达 + 执行器策略5 增强（轮询等待/force勾选/校验防假通过）+ 用例 step 12 改「保持立即开始」（选择器修正零匹配坑）；部署 5.60 | ✅ 本地 + 生产各连跑 3 次全绿、零残留，提交 `c0dedbdf` |
| 25 | 08-19 | **新建会议用例执行失败根因定位**：智能定位通过但执行 step 11 失败 → 页面直达跳过导航 + 直达后表单懒加载 + checkbox `is-hidden` 隐藏列 | ✅ 根因已定位（会话26 已修复闭环） |
| 24 | 08-18 | **finally api_call 端点真实化 + 完整 17 步验收**：真实端点 + 浏览器 token 自动注入 + `body_not_contains` 断言 + 修复 finally 双跑 | ✅ 完整 17 步 5.60 全绿、零残留，阶段 6 完成 |
| 23 | 08-18 | **语义化机制部署 5.60 + 生产验证**：37 后端文件 + 前端 dist 部署，5.60 网页连跑两次全绿、零残留 | ✅ 步骤15 修复生产确认，提交 `345572f2`+`06b6d9c0` |
| 22 | 08-18 | **语义化机制真实验证**：16 步主流程连跑两次全绿 + 零残留 + 修复步骤15 页面识别根因 | ✅ 验证通过，提交 `345572f2` |
| 21 | 08-18 | **语义化机制阶段6**：前端语义编辑 + 语义化重写示范用例 + 参数持久化修复 | ✅ 代码已提交 `7cf4f03f` |
| 20 | 08-18 | P1 三处映射修复（步骤14/15/16）+ 部署5.60 + API/UI 双链路端到端验证 | ✅ 21 步 100% 通过，无残留 |
| 19 | 08-17 | 部署会话17+18 改动到服务器 5.60（22 后端文件 + 前端 dist） | ✅ 已部署 |
| 18 | 08-17 | 被测系统 URL 配置统一功能 | ✅ 已部署 |
| 17 | 08-17 | 步骤12「预定会议」映射错配修复 | ✅ 已修复+部署 |
| 16 | 08-14/17 | 新服务器 5.69 自动化部署包验证 | ✅ 完成 |
| 15 | 08-13 | 步骤11 会议室选择"假通过"根因分析（裸 checkbox + force 点击） | 🔴 发现根因 |
| 14 | 08-13 | P0 修复：Phase4 登录检测 + 会议室选择映射 | ✅ 完成 |
| 13 | 08-12 | 会议室选择修复 + 新阻塞（Phase4 登录检测失效） | 🔴 发现阻塞 |
| 12 | 08-11 | URL 直达执行验证（新建会议 15 步通过，583.85s→253.85s） | ✅ 验证通过 |
| 11 | 08-11 | 方案1 元素映射表 Phase1-4 完整实施 + 部署 | ✅ 完成 |
| 10 | 08-11 | 方案1（元素映射表）分析 + PRD/执行计划 | ✅ 完成 |
| 9 | 08-10 | 智能定位 5 个核心缺陷修复 + 策略可行性评估 | ✅ 完成 |
| 7 | 08-07 | 智能定位 Phase1-6 端到端验证 + 部署 5.60 | ✅ 完成 |
| 6 | 08-06 | 智能定位准确率提升 Phase1-6 代码实现 | ✅ 完成 |
| 4 | 08-06 | Claude 语义增强实现 + 部署 | ✅ 完成 |
| 3 | 08-06 | 智能定位微前端（iframe）支持修复 + Claude 增强设计 | ✅ 完成 |
| 2 | 08-06 | 智能定位优化 + 用例串行执行 + 模块选择器搜索 | ✅ 完成 |
| 1 | 08-05 | 验证类步骤自动识别 | ✅ 完成 |

---

## ⚠️ 关键约束（绝对不要重复踩）

| # | 约束 | 说明 |
|---|------|------|
| 1 | **Playwright 必须同步 API** | Windows 异步 API 无法在 asyncio 循环启动。`main.py` 已配 ProactorEventLoop；`playwright_executor` 用 `sync_playwright`；`execution_service` 经 `run_in_executor` 线程池执行 |
| 2 | **工作线程 `asyncio.set_event_loop(None)`** | Playwright 用 `get_running_loop()` 检测，线程中必须置 None 绕过（`start()` 内） |
| 3 | **每用例新建 PlaywrightExecutor 实例** | 共享实例 `stop()` 后 `_is_running=False`，下次 `start()` 失败 |
| 4 | **前端 axios baseURL 必须空字符串** | 路径写完整 `/api/...`，否则 `/api/api/cases` 404 |
| 5 | **`useRoute()` 必须在 setup 顶层** | computed 内调用会报 `inject() can only be used inside setup()`，页面空白 |
| 6 | **前端 axios 超时 5 分钟** | Playwright 执行可能几分钟，`request.ts` 已配 300000ms |
| 7 | **SQLite 并发写锁** | 已配 `check_same_thread=False` + `pool_pre_ping=True`；`error_message` 必须 nullable |
| 8 | **uvicorn 勿加 `--reload`（Windows）** | reload 模式 WatchFiles 卡死，旧进程继续服务导致"改代码不生效"。改后必须重启进程 |
| 9 | **MySQL 密码勿含 `@`** | URL 解析歧义；生产用 `PlatApp2026`；ID 字段 `String(64)` |
| 10 | **语义化开关（settings flag）** | `SEMANTIC_ENABLED` / `USE_KEYWORD_MATCHER` / `USE_FUZZY_MAPPING`（默认 false）/ `ENFORCE_VERIFICATION`，可一键回切旧范式 |

---

## 🔑 关键配置信息

| 项目 | 值 |
|------|-----|
| 服务器 IP | 192.168.5.60（Docker 容器化，`plat-auto-test-app`） |
| SSH 用户/密码 | ubains / **Ubains@123** |
| 部署目录 | `/data/third_party/plat-auto-test/` |
| 前端 / API / 健康检查 | http://192.168.5.60 / `/docs` / `/health` |
| MySQL 外部访问 | 192.168.5.60:3307 · platapp / **PlatApp2026** |
| 被测系统 | https://192.168.5.44（微前端 micro-app） |
| 登录凭据 | admin@xty / **Ubains@13579** · 验证码固定 `csba` |
| 远程仓库 | http://git.ubainsyun.com/bing/ubains-module-test.git |

---

## 🚀 启动与运维

```bash
# 后端（端口 8001；勿加 --reload）
cd backend && pip install -r requirements.txt && playwright install chromium
uvicorn app.main:app --port 8001

# 前端（端口 3000，代理指向 8001）
cd frontend && npm install && npm run dev

# 测试
cd backend && pytest tests/ -v --cov=app --cov-report=html   # 预存坏的 test_api.py 需 --deselect
cd frontend && npm run build
```

**登录模板**：用例 `config.auto_login: true` 走 login_templates 表模板，无需显式登录步骤。

**智能定位**：用例管理页 → 选择用例 → 「智能定位」按钮；API `POST /api/element/smart-locate`。

---

## 📑 文档索引

| 文档 | 路径 |
|------|------|
| 项目指南 | `CLAUDE.md` |
| 多窗口并行开发指南 | `Docs/多窗口并行开发指南.md` |
| 主 PRD | `Docs/PRD/需求文档/总览/_PRD_平台自动化测试可视化系统需求文档.md` |
| 🔴 **语义化用例执行机制 PRD** | `Docs/PRD/需求文档/用例管理/_PRD_复杂用例通用执行机制_语义化用例执行.md` |
| 🔴 **语义化用例执行机制执行计划** | `Docs/PRD/需求文档/用例管理/_执行计划_复杂用例通用执行机制_语义化用例执行.md` |
| 智能定位 PRD/执行计划 | `Docs/PRD/需求文档/用例管理/_PRD_自然语言用例智能定位功能.md` 等 |
| 元素映射表定位方案 PRD/执行计划 | `Docs/PRD/需求文档/用例管理/_PRD_元素映射表定位方案_前端key-value键值_提升定位稳定性.md` |
| 🔴 **定时任务模块 + 系统配置被测系统URL PRD** | `Docs/PRD/需求文档/定时任务/_PRD_需求文档_UI自动化定时任务模块与系统配置被测系统URL.md` |
| 🔴 **定时任务模块执行计划** | `Docs/PRD/需求文档/定时任务/_PRD_需求文档_UI自动化定时任务模块与系统配置被测系统URL_计划执行.md` |
| 🔴 新建会议执行失败问题处理 | `Docs/PRD/问题处理/会议管理/_问题处理_新建会议用例执行失败_页面直达后会议室表格checkbox不可见.md` |
| 🔴 新建会议执行失败执行计划 | `Docs/PRD/问题处理/会议管理/_执行计划_修复新建会议用例执行失败.md` |
| HANDOFF 总交接 | `HANDOFF.md` |
| V2 部署交接 | `HANDOFF_V2部署升级.md` |
| 安全测试交接 | `HANDOFF_安全测试.md` |

---

## ✅ 待办事项（仅未完成）

### P0（高优先级）

- [x] **修复「执行中无法取消」（MySQL 1205 行锁）+ SUT URL 校验加固**（会话29 实现 + 部署）
  - ✅ 置 running 后立即 commit 释放 InnoDB 行锁（取消接口 91ms 返回，修复前 ~50s 1205 超时）
  - ✅ 实机发现身份映射过期实例覆盖 cancelled 为 completed → 收尾状态判定改列查询 + FOR UPDATE，回归测试覆盖
  - ✅ `_validate_sut_url` 拒绝非法地址（192.168.5.4202 段越界 / 非法端口 / 非法字符），前端同步表单校验
  - ✅ 后端 186 测试全过；**已部署 5.60 生产**，实机取消演练通过、恢复用例跑通 1/1 100%
- [x] **系统配置切换被测系统后用例自动跟随（方案A·执行期地址重写）**（会话28 实现 + 部署）
  - ✅ 执行器 `_rebase_sut_url`：导航/接口步骤若指向已知被测系统主机（默认 5.44）→ 自动替换为当前配置 SUT 地址（保留 scheme/路径/查询/hash）
  - ✅ 130 条存量硬编码 `https://192.168.5.44` 的 UI 用例全部自动跟随，**不动任何用例数据**
  - ✅ 后端 182 测试全过（新增 `TestRebaseSutUrl` 3 条回归）；**已部署 5.60 生产**（1 后端文件 → 容器重启 healthy）
  - ✅ 效果：系统配置保存新 SUT → 后续手动/定时执行导航与接口自动打到新地址
- [x] **UI自动化定时任务模块 + 系统配置被测系统URL**（会话27 实现 + 本地验收）
  - ✅ 调度引擎（30s 轮询/到期异步触发/冲突跳过/防重入）+ 定时任务 CRUD/启停/立即执行/运行记录
  - ✅ 报告按模块分组 + 被测系统 URL + 触发方式列；SUT URL 保存/重启持久化
  - ✅ 后端 179 测试全过、前端 build 通过、真实端到端验收通过
  - ✅ **已部署 5.60 生产**（2026-08-19：13 后端文件 + 前端 dist 90 文件上传 → 容器重启 healthy；调度引擎启动、`/api/scheduled-tasks` 与 `/api/system/sut-config` 端点正常、定时任务菜单/页面/系统配置被测系统卡片在 5.60 网页验证通过；5.60 被测系统 URL 当前为 env 默认 `https://192.168.5.44`，在系统配置页保存一次即持久化到 MySQL）
- [x] **阶段6 最终验收：完整 17 步示范用例（含 finally 自清理）**（会话24 完成）
  - ✅ **16 步主流程真实验证通过**（本地会话22 + 5.60 生产会话23，各连跑两次全绿、零残留）——提交 `345572f2`
  - ✅ **已部署 5.60 生产**（37 后端文件 + 前端 dist，含 semantic_resolver / ui_context / 映射表 v2 / 前端语义编辑）——部署脚本 `backend/_deploy_560_semantic.py`
  - ✅ **finally api_call 端点真实化**：`GET /meetingV3/api/message/getMessagePageList` + 浏览器 token 自动注入 + `body_not_contains` 断言
  - ✅ **完整版（17 步含 finally 自清理）5.60 真实执行验收通过**（`exec_a8dbb9c4`，96.1s，全绿、零残留）
- [ ] **修复 3 个预存映射问题**（2026-08-13 确认，非 P0 回归）
  - `点击选择时间` 误配 → 会议列表-全部-时间（期望：创建会议-选择时间）
  - `点击会议室列表` 误配 → 首页-置顶功能-会议室列表（期望：创建会议-会议室列表）
  - `勾选房间` 误配 → 会议室-时间轴（期望：会议室-房间勾选）
- [x] **修复新建会议用例 step 11 执行失败（页面直达 + checkbox 隐藏，根因已定位）**（会话26 完成）
  - ✅ 用例 `case_13650e0406e64779b66e6fa5de35c24a`，智能定位 17/18 通过但执行仍失败
  - ✅ 根因：执行期页面直达跳过导航 + 直达后表格懒加载（12s-130s+）+ 会议室表格仅列空闲房间 + checkbox `is-hidden` 隐藏列；step 17 根因：step 12 切预定会议→无结束会议按钮
  - ✅ 方案（最终）：**保留页面直达** + 执行器策略5 增强（轮询等待 150s + force 勾选 + 校验）+ 用例 step 12 改「保持立即开始」（选择器 `.time_btn > div:first-child`）
  - ✅ 验收：本地 + 生产 5.60 各连跑 3 次 18/18 全绿、零残留，提交 `c0dedbdf`

### P1（中优先级）

- [ ] 前端适配验证步骤的 `verify_method` 参数
- [ ] 增加更多菜单的定位器优化（滚动、备选选择器）

### P2（低优先级）

- [ ] 完善二级菜单映射表（`menu_mapping.py`）
- [ ] 智能定位 API 与用例执行路径统一（当前两者逻辑分离）
- [ ] Claude 调用性能优化（缓存结果、减少超时时间）
- [ ] 维护 jieba 自定义词典（专业术语分词优化）
- [ ] Docker 容器安装中文字体（修复截图乱码，不影响定位）

---

*本文档由 Claude Code 于 2026-08-19 更新（会话 29：修复「执行中无法取消」（MySQL 1205 行锁，置 running 后立即 commit 释放行锁）+ 实机发现身份映射过期实例覆盖 cancelled 为 completed（改列查询 + FOR UPDATE）+ SUT URL 校验加固，后端 186 测试全过、实机取消演练通过、已部署 5.60 生产）。*
