提交 ac823858 authored 作者: 陈泽健's avatar 陈泽健

feat(executor): 系统配置切换被测系统后用例执行自动跟随新地址

方案A·执行期地址重写:执行器新增 _rebase_sut_url + KNOWN_SUT_HOSTS,
导航/接口步骤若指向已知被测系统主机(默认 https://192.168.5.44)则自动
替换为当前配置的 SUT 地址(保留 scheme/路径/查询/hash),130 条存量
硬编码用例自动跟随、不动任何数据;新增 TestRebaseSutUrl 3 条回归测试;
已部署 5.60 生产(容器内真实验证通过)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 9d3c24b9
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
> **生成时间**: 2026-08-19 > **生成时间**: 2026-08-19
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **最近提交**: `1bb8756a` chore: .gitignore 遮蔽历史会话遗留的一次性脚本与临时输出 > **最近提交**: `1bb8756a` chore: .gitignore 遮蔽历史会话遗留的一次性脚本与临时输出
> **状态**: 🟢 **会话27:UI自动化定时任务模块 + 系统配置被测系统URL 已实现并本地全链路验证通过——调度引擎(30s 轮询 + 到点异步触发 + 冲突跳过)复用现有执行引擎生成 `trigger_type=scheduled` 执行记录,报告按模块分组并展示被测系统 URL;SUT URL 保存后重启持久化。后端 179 测试全过、前端 build 通过、真实端到端验收通过** > **状态**: 🟢 **会话28:系统配置切换被测系统后用例自动跟随(方案A·执行期地址重写)已实现并部署 5.60 生产——执行器新增 `_rebase_sut_url`,导航/接口步骤若指向已知被测系统主机(默认 5.44)则自动替换为当前配置的 SUT 地址,130 条存量硬编码用例自动跟随,不动任何数据。后端 182 测试全过、已部署 5.60**
--- ---
...@@ -180,7 +180,34 @@ ...@@ -180,7 +180,34 @@
- git-bash 的 `/tmp` 与 Windows Python 的 `/tmp` 指向不同路径(`C:\tmp` vs Git 安装目录)——跨工具传 JSON 文件务必用显式绝对路径 - git-bash 的 `/tmp` 与 Windows Python 的 `/tmp` 指向不同路径(`C:\tmp` vs Git 安装目录)——跨工具传 JSON 文件务必用显式绝对路径
- 多窗口并行时注意与另一后端共用 SQLite,避免并发写锁 - 多窗口并行时注意与另一后端共用 SQLite,避免并发写锁
**遗留**:验证用定时执行记录 + 报告留在开发库(`verify-interval-1min-20260819-140054`,可作演示数据);**尚未部署 5.60 生产**(按用户要求待后续)。 **遗留**:验证用定时执行记录 + 报告留在开发库(`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 → 后续用例执行(手动/定时)的导航与接口步骤自动打到新地址。
--- ---
...@@ -188,7 +215,8 @@ ...@@ -188,7 +215,8 @@
| 会话 | 日期 | 主题 | 结果 | | 会话 | 日期 | 主题 | 结果 |
|------|------|------|------| |------|------|------|------|
| 27 | 08-19 | **UI自动化定时任务模块 + 系统配置被测系统URL**:调度引擎(30s 轮询 + 到期异步触发 + 冲突跳过)+ 定时任务 CRUD/启停/立即执行/运行记录 + 报告按模块分组并展示被测系统 URL + SUT URL 保存/重启持久化 + 前端定时任务页与系统配置卡片 | ✅ 按 PRD 工作流实现,后端 179 测试全过、前端 build 通过、真实端到端验收通过(未部署生产) | | 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` | | 26 | 08-19 | **新建会议用例 step 11/17 执行失败修复**:保留页面直达 + 执行器策略5 增强(轮询等待/force勾选/校验防假通过)+ 用例 step 12 改「保持立即开始」(选择器修正零匹配坑);部署 5.60 | ✅ 本地 + 生产各连跑 3 次全绿、零残留,提交 `c0dedbdf` |
| 25 | 08-19 | **新建会议用例执行失败根因定位**:智能定位通过但执行 step 11 失败 → 页面直达跳过导航 + 直达后表单懒加载 + checkbox `is-hidden` 隐藏列 | ✅ 根因已定位(会话26 已修复闭环) | | 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 完成 | | 24 | 08-18 | **finally api_call 端点真实化 + 完整 17 步验收**:真实端点 + 浏览器 token 自动注入 + `body_not_contains` 断言 + 修复 finally 双跑 | ✅ 完整 17 步 5.60 全绿、零残留,阶段 6 完成 |
...@@ -294,11 +322,16 @@ cd frontend && npm run build ...@@ -294,11 +322,16 @@ cd frontend && npm run build
### P0(高优先级) ### P0(高优先级)
- [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 实现 + 本地验收) - [x] **UI自动化定时任务模块 + 系统配置被测系统URL**(会话27 实现 + 本地验收)
- ✅ 调度引擎(30s 轮询/到期异步触发/冲突跳过/防重入)+ 定时任务 CRUD/启停/立即执行/运行记录 - ✅ 调度引擎(30s 轮询/到期异步触发/冲突跳过/防重入)+ 定时任务 CRUD/启停/立即执行/运行记录
- ✅ 报告按模块分组 + 被测系统 URL + 触发方式列;SUT URL 保存/重启持久化 - ✅ 报告按模块分组 + 被测系统 URL + 触发方式列;SUT URL 保存/重启持久化
- ✅ 后端 179 测试全过、前端 build 通过、真实端到端验收通过 - ✅ 后端 179 测试全过、前端 build 通过、真实端到端验收通过
- [ ] **待部署 5.60 生产**(用户后续要求时执行;开发库留有验证用执行记录与报告 - **已部署 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 完成) - [x] **阶段6 最终验收:完整 17 步示范用例(含 finally 自清理)**(会话24 完成)
-**16 步主流程真实验证通过**(本地会话22 + 5.60 生产会话23,各连跑两次全绿、零残留)——提交 `345572f2` -**16 步主流程真实验证通过**(本地会话22 + 5.60 生产会话23,各连跑两次全绿、零残留)——提交 `345572f2`
-**已部署 5.60 生产**(37 后端文件 + 前端 dist,含 semantic_resolver / ui_context / 映射表 v2 / 前端语义编辑)——部署脚本 `backend/_deploy_560_semantic.py` -**已部署 5.60 生产**(37 后端文件 + 前端 dist,含 semantic_resolver / ui_context / 映射表 v2 / 前端语义编辑)——部署脚本 `backend/_deploy_560_semantic.py`
...@@ -329,4 +362,4 @@ cd frontend && npm run build ...@@ -329,4 +362,4 @@ cd frontend && npm run build
--- ---
*本文档由 Claude Code 于 2026-08-19 更新(会话 27:UI自动化定时任务模块 + 系统配置被测系统URL——调度引擎 + 定时任务管理 + 报告按模块分组 + SUT URL 持久化,后端 179 测试全过、前端 build 通过、真实端到端验收通过,未部署生产)。* *本文档由 Claude Code 于 2026-08-19 更新(会话 28:系统配置切换被测系统后用例自动跟随(方案A·执行期地址重写)——`_rebase_sut_url` 导航/接口主机重写,130 条存量硬编码用例自动跟随,后端 182 测试全过,已部署 5.60 生产)。*
...@@ -15,6 +15,7 @@ import re ...@@ -15,6 +15,7 @@ import re
import asyncio import asyncio
import time import time
from typing import Optional, Callable, Dict, Any, List from typing import Optional, Callable, Dict, Any, List
from urllib.parse import urlparse, urlunsplit
from datetime import datetime from datetime import datetime
from dataclasses import dataclass, field from dataclasses import dataclass, field
...@@ -134,6 +135,12 @@ class PlaywrightExecutor: ...@@ -134,6 +135,12 @@ class PlaywrightExecutor:
"captcha": "csba", "captcha": "csba",
} }
# 已知被测系统主机集合(方案A·SUT地址跟随):
# 导航/接口步骤若指向这些主机,执行时自动替换为当前配置的 SUT 主机(self.base_url)。
# 默认从 LOGIN_CONFIG 派生(https://192.168.5.44),覆盖全部存量用例;
# 未来若切到其他被测系统并回切,可在此追加主机(如 "192.168.5.69")。
KNOWN_SUT_HOSTS = {urlparse(LOGIN_CONFIG["base_url"]).netloc}
def __init__(self, config: Optional[Dict[str, Any]] = None): def __init__(self, config: Optional[Dict[str, Any]] = None):
""" """
初始化执行器 初始化执行器
...@@ -1430,6 +1437,37 @@ class PlaywrightExecutor: ...@@ -1430,6 +1437,37 @@ class PlaywrightExecutor:
# ==================== 动作实现 ==================== # ==================== 动作实现 ====================
def _rebase_sut_url(self, url: str) -> str:
"""
被测系统地址跟随(方案A·执行期重写):
若目标 URL 的主机属于 KNOWN_SUT_HOSTS(默认 https://192.168.5.44),
则将其主机替换为当前配置的 self.base_url 主机(保留 scheme/路径/查询/hash)。
作用:在系统配置中切换被测系统后,存量与新建用例的导航/接口步骤自动跟随,
无需批量修改用例数据;非被测系统的外部地址不受影响。
Args:
url (str): 原始目标 URL
Returns:
str: 重写后的 URL(无需改写时原样返回)
"""
try:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return url
if parsed.netloc not in self.KNOWN_SUT_HOSTS:
return url
base = urlparse(self.base_url)
if not base.netloc or base.netloc == parsed.netloc:
return url # 已指向当前配置地址,无需改写
return urlunsplit(
(parsed.scheme, base.netloc, parsed.path, parsed.query, parsed.fragment)
)
except Exception as e:
logger.debug(f"[SUT跟随] URL 重写失败(保留原样): {e}")
return url
def _do_navigate(self, params: dict) -> None: def _do_navigate(self, params: dict) -> None:
""" """
页面导航 页面导航
...@@ -1440,6 +1478,8 @@ class PlaywrightExecutor: ...@@ -1440,6 +1478,8 @@ class PlaywrightExecutor:
url = params.get("url", "") url = params.get("url", "")
if not url: if not url:
raise ValueError("导航URL不能为空") raise ValueError("导航URL不能为空")
# 系统配置切换被测系统后,导航地址自动跟随(硬编码旧 SUT 地址的存量用例同样生效)
url = self._rebase_sut_url(url)
current_url = self._page.url current_url = self._page.url
# 如果只是 hash 变化(同源),用 JS 修改 location 更可靠, # 如果只是 hash 变化(同源),用 JS 修改 location 更可靠,
# 避免 goto 等待 navigation 事件在 SPA hash 路由上超时 # 避免 goto 等待 navigation 事件在 SPA hash 路由上超时
...@@ -2157,6 +2197,11 @@ class PlaywrightExecutor: ...@@ -2157,6 +2197,11 @@ class PlaywrightExecutor:
if isinstance(rendered, dict): if isinstance(rendered, dict):
api_case = rendered api_case = rendered
# 系统配置切换被测系统后,接口地址自动跟随(与导航步骤同一套重写规则)
steps = api_case.get("steps") or {}
if isinstance(steps, dict) and steps.get("url"):
steps["url"] = self._rebase_sut_url(str(steps["url"]))
# 被测系统接口需鉴权(无 token 返回 401 令牌不能为空): # 被测系统接口需鉴权(无 token 返回 401 令牌不能为空):
# 目标为 TARGET_URL 且未显式指定鉴权头时,自动从浏览器会话注入 # 目标为 TARGET_URL 且未显式指定鉴权头时,自动从浏览器会话注入
# localStorage.meet_token —— finally 数据自清理复用主流程已建立的登录态。 # localStorage.meet_token —— finally 数据自清理复用主流程已建立的登录态。
......
...@@ -264,3 +264,30 @@ class TestDoApiCall: ...@@ -264,3 +264,30 @@ class TestDoApiCall:
with patcher: with patcher:
result = ex._do_api_call({}) result = ex._do_api_call({})
assert result.status == "passed" assert result.status == "passed"
# ==================== 方案A·SUT地址跟随(_rebase_sut_url) ====================
class TestRebaseSutUrl:
"""系统配置切换被测系统后,导航/接口地址自动跟随(执行期重写,不改用例数据)"""
def test_switch_sut_rebases_hardcoded_legacy(self):
"""存量用例硬编码 192.168.5.44 → 跟随切换到 192.168.5.69(保留路径/查询/hash/scheme)"""
ex = make_executor(base_url="https://192.168.5.69")
assert ex._rebase_sut_url("https://192.168.5.44") == "https://192.168.5.69"
assert ex._rebase_sut_url("https://192.168.5.44/#/platform") == "https://192.168.5.69/#/platform"
assert ex._rebase_sut_url("https://192.168.5.44/login?x=1&y=2") == "https://192.168.5.69/login?x=1&y=2"
assert ex._rebase_sut_url("http://192.168.5.44/api/meeting") == "http://192.168.5.69/api/meeting"
def test_default_sut_unchanged(self):
"""未切换 SUT(默认 5.44)时地址原样,绝不误改写"""
ex = make_executor() # base_url 默认 192.168.5.44
assert ex._rebase_sut_url("https://192.168.5.44/x") == "https://192.168.5.44/x"
assert ex._rebase_sut_url("https://192.168.5.69/x") == "https://192.168.5.69/x"
def test_relative_and_external_untouched(self):
"""相对路径 / 外部链接 不受影响"""
ex = make_executor(base_url="https://192.168.5.69")
assert ex._rebase_sut_url("/relative/path") == "/relative/path"
assert ex._rebase_sut_url("https://example.com/docs") == "https://example.com/docs"
assert ex._rebase_sut_url("") == ""
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论