From 27c1cbed9269eb5550dc224f24427c718e578a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=93=E4=B8=87=E9=B9=8F?= Date: Wed, 10 Jun 2026 18:06:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E9=87=8D=E6=9E=84SKILL=E4=B8=BA=E9=80=9A?= =?UTF-8?q?=E7=94=A8SKILL,=E9=80=82=E9=85=8D=E6=89=80=E6=9C=89`python=20+?= =?UTF-8?q?=20pytest=20+=20requests`=E7=9A=84=E6=8E=A5=E5=8F=A3=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=E6=B5=8B=E8=AF=95=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- README.md | 187 +++++++++++--------------- SKILL.md | 10 +- capture/README.md | 10 +- capture/capture_addon.py | 11 +- config.json | 39 +++++- doc/core_principles.md | 10 +- doc/mode_maintenance_pytest_driven.md | 21 ++- doc/preflight_gates_maintenance.md | 22 ++- doc/preflight_gates_new.md | 22 ++- skill_utils/__init__.py | 16 ++- skill_utils/api_path_match.py | 17 ++- skill_utils/config_loader.py | 157 +++++++++++++++++++++ skill_utils/project_root.py | 49 +++---- skill_utils/pytest_command.py | 30 +++++ tools/api_extract_rules.json | 6 + tools/append_extract_rule.py | 126 +++++++++++++++++ tools/init_project_scan.py | 124 +++++++++++++++++ tools/page_api_index.sqlite3 | Bin 827392 -> 827392 bytes tools/scan_page_api.py | 153 +++++++++++++++------ 20 files changed, 795 insertions(+), 219 deletions(-) create mode 100644 skill_utils/config_loader.py create mode 100644 skill_utils/pytest_command.py create mode 100644 tools/api_extract_rules.json create mode 100644 tools/append_extract_rule.py create mode 100644 tools/init_project_scan.py diff --git a/.gitignore b/.gitignore index 0bc0c0e..9c07328 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ __pycache__/ *.pyc *.pyo -# Claude 本地配置 -.claude/ +# IDE 本地配置 +.idea/ # 临时文件 temp_*.py diff --git a/README.md b/README.md index f803bae..97a9e91 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,42 @@ -# api-test-E10 +# api-test-common -`test-automation` 仓库**内置的接口自动化编写 Skill**(项目级,物理位置 `.claude/skills/api-test-E10/`),提供“新增 / 维护”两类任务入口;新增任务提供四种编写方式,维护任务提供四种维护方式。 -AI 执行规范详见 [`SKILL.md`](./SKILL.md),完整流程图详见 [`flow_chart/flow.md`](./flow_chart/flow.md)。 - -# AI接口自动化测试框架推荐 - -项目地址:https://github.com/buer2233/ai-api-test +> 一个面向 **所有 `python + pytest + requests` 接口自动化测试框架** 的 AI Skill:把接口方法扫描、用例编写、抓包分析、pytest 失败维护和项目编码风格约束串成可落地的自动化工作流。 -基于当前项目的SKILL,结合通用的接口自动化测试框架编写的,通用的AI接口自动化框架。目前为初版状态,还在持续优化迭代中,感兴趣的大佬可以点个星星关注下。 +AI 执行规范详见 [`SKILL.md`](./SKILL.md),完整流程图详见 [`flow_chart/flow.md`](./flow_chart/flow.md)。 -# 实际的效率提升记录和使用记录 +如果你的接口自动化项目满足这三个条件:**Python 编写、pytest 执行、requests 发请求**,这个 Skill 就可以通过 `config.json` 接入你的项目,而不是被某一个固定目录、固定包名或固定业务系统绑定。 -## 数据提升记录 +它的目标不是再造一个测试框架,而是让 AI 更稳定地在你现有的接口自动化框架里工作:读懂现有接口层、沿用已有编码风格、避免重复造接口方法、按真实 pytest 结果闭环维护。 -数据来源于我个人工作中负责的EB低代码模块使用AI后的5个月,和使用AI前的5个月。 +## 为什么值得关注 -通过AI+SKILL编写接口用例的效率提升90%,综合效率提升46%。 +- **框架无侵入接入**:通过 `config.json` 指定项目根、接口方法目录、用例目录、pytest 工作目录和扫描目录;不要求你迁移项目结构。 +- **适配真实项目写法**:支持直接 `requests.*`、`requests.request(...)`、`self.get/post/...`、`self.request("METHOD", ...)`、`send_msg(...)` 等常见封装,并支持后续追加特殊提取规则。 +- **自动建立 API 索引**:扫描现有接口方法,生成 `tools/page_api_index.sqlite3`,用于查重、抓包匹配、源码分析和新增用例前的接口覆盖判断。 +- **先学习你的编码风格**:初始化扫描时基于你提供的接口方法模板和 pytest 用例模板生成 `coding_style_guide` 草稿,让 AI 按你的项目风格写代码。 +- **新增和维护都能跑通闭环**:新增任务支持抓包、参考已有用例、cURL、Java Controller 源码四种入口;维护任务支持抓包回溯、参考用例、cURL、pytest 报错驱动四种方式。 +- **pytest 结果优先**:维护时以真实 pytest 报错分类,区分“功能 BUG / 用例待维护 / 信息不足”,避免为了通过测试盲目改断言。 +- **面向 Windows 友好**:路径、中文目录、PowerShell pytest 命令、UTF-8 编码和抓包运行目录都做了显式约束。 +- **保留安全门禁**:新增/维护任务有明确前置清单,特殊接口提取规则采用“预览 → 用户确认 → 增量入库”,不偷偷改库。 -| 月份 | 周次 | 接口方法数 | 接口用例数 | 接口+用例总数 | AI使用 | -| --------------------- | -------------- | ---------- | ---------- | ------------- | ------ | -| 未使用AI前的5周数据 | 3月第四周 | 51 | 34 | 85 | 否 | -| | 3月第五周 | 5 | 29 | 34 | 否 | -| | 4月第一周 | 45 | 18 | 63 | 否 | -| | 4月第二周 | 26 | 38 | 64 | 否 | -| | 4月第三周 | 2 | 79 | 81 | 否 | -| | 合计 | 129 | 198 | 327 | | -| | | | | | | -| 使用AI提效后的5周数据 | 4月第四周 | 34 | 84 | 118 | 是 | -| | 5月第一周 | 28 | 58 | 86 | 是 | -| | 5月第二周 | 1 | 53 | 54 | 是 | -| | 5月第三周 | 31 | 109 | 140 | 是 | -| | 5月第四周 | 2 | 79 | 81 | 是 | -| | 合计 | 96 | 383 | 479 | | -| | | | | | | -| | 使用AI的提升率 | 74.42% | 193.43% | 146.48% | | +## 适配范围 -## 通过AI+SKILL编写自动化测试用例记录 +当前 Skill 专注一个清晰边界:`python + pytest + requests` 接口自动化测试框架。 - 《通过AI+SKILL编写自动化测试用例记录》 +| 支持 | 暂不支持 | +|---|---| +| pytest 用例组织与执行 | unittest | +| requests 及基于 requests 的轻量封装 | httpx / aiohttp | +| 现有 page_api / api_client / service_api 等接口封装目录 | 非 Python 接口测试框架 | +| 抓包、cURL、参考用例、Java Controller 源码辅助编写 | 通用 UI 自动化、性能测试、契约测试平台 | -## 通过AI+SKILL维护用例的测试记录 +## 典型使用路径 - 《通过AI+SKILL维护用例的测试记录》 +1. 在 [`config.json`](./config.json) 中配置你的测试框架路径。 +2. 选择接口方法模板和 pytest 用例模板,运行初始化扫描,生成编码风格草稿和 API 索引。 +3. 新增接口自动化用例时,按前置清单提供接口方法文件、用例文件和用例名,选择抓包 / 参考用例 / cURL / Controller 源码方式。 +4. 维护失败用例时,指定用例位置,AI 按最新 pytest 报错定位并分类处理。 +5. 每次改动都以目标 pytest 命令和关键日志收尾,避免只生成代码不验证。 ## 环境要求与安装 @@ -53,10 +48,58 @@ AI 执行规范详见 [`SKILL.md`](./SKILL.md),完整流程图详见 [`flow_ch | pytest 失败修复 | 维护方式④默认优先使用 | `/test-fixing` | `npx skills add sickn33/antigravity-awesome-skills@test-fixing -g -y`
GitHub: `https://github.com/sickn33/antigravity-awesome-skills` | | Python 断点调试 | `/test-fixing` 无法解决、调用栈或前后接口信息不明确时兜底使用 | `/Debugging` | `npx skills add pluginagentmarketplace/custom-plugin-python@debugging -g -y`
GitHub: `https://github.com/pluginagentmarketplace/custom-plugin-python` | -`api-test-E10` 已随 `test-automation` 仓库一起分发,clone 仓库后无需额外安装。Claude Code 会自动从项目 `.claude/skills/` 目录加载本 skill。 +使用前必须先初始化本目录下的 [`config.json`](./config.json),把 `project_root`、API 方法目录、pytest 工作目录、用例目录和索引扫描目录改成目标接口自动化项目的真实路径。 第三方依赖 Skill 需要在当前 AI 工具环境中可用:`/test-fixing` 用于默认测试修复流程,`/Debugging` 用于维护困难时通过断点、执行堆栈、局部变量、请求 payload、接口响应和方法返回值辅助定位。 +## 使用前:初始化 config.json + +本 skill 不再从固定目录或项目 marker 推导项目根。所有路径都从 `config.json` 读取。 + +最小必填项: + +```json +{ + "framework": "pytest_requests", + "project_root": "D:/your-api-test-project", + "paths": { + "api_method_dirs": ["tests/page_api"], + "test_case_dirs": ["tests/cases"], + "pytest_workdir": "tests", + "runtime_temp_dir": "runtime" + }, + "pytest": { + "pythonpath": ".", + "command_template": "pytest {target} -v --tb=short" + }, + "api_index": { + "db_path": "tools/page_api_index.sqlite3", + "extract_rules_path": "tools/api_extract_rules.json", + "scan_dirs": ["tests/page_api"], + "extract_rules": "builtin_requests_plus_generated" + } +} +``` + +`tools/api_extract_rules.json` 是内部规则文件,由初始化扫描或 AI 维护;用户无需手写正则。 + +历史项目路径只作为迁移参考,不再是默认硬规则。 + +初始化扫描示例: + +```powershell +python tools/init_project_scan.py ` + --config config.json ` + --api-template D:\your-api-test-project\tests\page_api\user_api.py ` + --case-template D:\your-api-test-project\tests\cases\test_user_api.py +``` + +扫描完成后会生成: + +- 编码风格草稿:`//coding_style_guide_draft.md` +- API 覆盖索引:`tools/page_api_index.sqlite3` +- 内部提取规则文件:`tools/api_extract_rules.json` + ## 使用前:填写任务信息 ### 新增用例任务 @@ -86,80 +129,6 @@ AI 执行规范详见 [`SKILL.md`](./SKILL.md),完整流程图详见 [`flow_ch - `[接口用例位置]` = `填写具体的待维护的单个/多个用例,例如:test_xxx / 某测试类下的多个用例 / 第456行附近的 xxx 用例` ``` -- **例外**:纯查询/工具/诊断类对话不需要填 - -> 因 skill 已固定安装在 `/.claude/skills/api-test-E10/`,项目根由 skill 自身位置直接推导,**不再需要 AI 在对应前置门禁通过后回写 `config.project_path`**。抓包与勾选工具自动把运行时产物落到 `/api_test_dwp_temp/`。 - -## 任务类型入口 - -AI 会先判断本次任务是: - -- **新增**:新增接口方法 / 新增用例 / 补齐新链路 -- **维护**:修复已有接口方法 / 更新已有用例 / 回溯最新链路 / 定点修补 - -确认任务类型后,再进入对应方式:新增任务四选一,维护任务四选一。 - -## 新增任务的四种编写方式 - -任务信息齐全后,AI 会让您选择以下方式(或根据任务信号自动推断): - -| 方式 | 流程概要 | 适合场景 | -|---|---|---| -| **① 抓包驱动** | UI 操作 → 抓包 JSONL → 勾选接口 → 分析抓包 → 设计用例 → 相似度检查 → 编写用例 → pytest | 新接口多 / 复杂链路 | -| **② 参考已有用例** | 指定参考用例 → AI 仿写 → pytest | 同类批量 / 修参数断言 | -| **③ cURL 手工** | 粘贴 cURL + 响应 → AI 解析生成 → pytest | 抓包不可用 / 数据过大 | -| **④ Java Controller 源码参考** | Controller/Jacoco → 提取接口 → 对照索引查重 → 生成可编辑分析草稿 → 用户调整勾选与分组 → 补齐方法/用例 → pytest | 后端已有接口定义但接口自动化未覆盖 | - - -> 详细决策树与每种方式的完整步骤见 [`flow_chart/flow.md`](./flow_chart/flow.md)。 - -## 维护任务的四种维护方式 - -维护任务信息齐全后,AI 会让您选择以下方式(或根据任务信号自动推断): - -| 方式 | 流程概要 | 适合场景 | -|---|---|---| -| **① 抓包驱动** | 最新抓包 → 回溯链路 → 对照现有实现 → 维护用例 → pytest | 链路变化大 / 多接口联动 | -| **② 参考已有用例** | 指定参考样本 → 对照差异 → 局部维护 → pytest | 同类用例结构稳定 / 参数断言调整 | -| **③ cURL 手工** | 粘贴 cURL + 响应 → 对照旧实现 → 定点维护 → pytest | 少量接口变化明确 | -| **④ pytest 报错驱动** | AI 直接执行目标用例 pytest → 按最后一个中断报错分类 → 用例待维护时优先 `/test-fixing` → 必要时 `/Debugging` 断点定位 → 循环验证 | 用户只想指定用例后让 AI 自行跑失败并维护 | - -### 新增任务方式① 快速上手 - -1. 双击 `capture/start.bat` 启动抓包(或让 AI 启动) -2. 浏览器代理 → `127.0.0.1:12138`,完成业务操作后回复"继续" -3. AI 生成勾选草稿 → 您勾选需要的接口 → AI 分析抓包、设计用例、检查相似用例 → 编写方法/用例 → pytest 闭环 - -### 新增任务方式② 快速上手 - -发送 `# 本次任务信息` + 参考样本(函数名或文件路径)+ 差异点描述 - -### 新增任务方式③ 快速上手 - -发送 `# 本次任务信息` + 每个接口的 cURL 命令 + 对应响应体 - -> 运行时产物(`latest.jsonl`、`capture_selection.md`)落在**项目根**的 `api_test_dwp_temp/` 下,**不在** skill 自身目录。 - -### 新增任务方式④ 快速上手 - -发送 `# 本次任务信息` + Controller 源码/Jacoco 报告链接/本地源码文件。AI 会先执行 `tools/analyze_java_controller.py`,生成项目根 `api_test_dwp_temp/java_sourceCode_analysisResult.md`。您调整 `[x]` / `[ ]`、接口分组或参考用例备注后,再让 AI 继续补齐接口方法与 `_CSC.py` 用例。 - -## 常见问题 - -**Q:12138 端口被占?** 运行 `capture/stop.bat`;仍失败则 `netstat -ano | findstr :12138` 查占用 PID。 - -**Q:mitmproxy HTTPS 仍告警?** 99% 是证书装到"当前用户"而非"本地计算机",参考 `capture/README.md` 重装。 - -**Q:抓不到 `/oa/second` 下请求?** 检查 `config.py` 中 `RunConfig.baseurl` 是否与浏览器访问域名一致。 - -**Q:抓包数据太多?** 在 `capture/allowed_prefixes.txt` 删减前缀,或在勾选草稿中只勾必要接口。 - -**Q:抓包含敏感信息吗?** `Cookie`/`Authorization` 头仅保留前 20 字符 + 长度摘要,不落全量。建议定期清理项目根下 `api_test_dwp_temp/latest.jsonl`。 - -**Q:不想用抓包?** 可用方式②(参考已有用例)或方式③(cURL 手工)。 - -**Q:抓包数据落到 skill 目录而不是项目目录?** 本版本 skill 已固定安装在 `/.claude/skills/api-test-E10/`,项目根由 skill 位置直接推导。如发现产物落到 skill 目录,请确认目录路径符合该结构、且项目根下存在 `E10自动化` 子目录。 - ## 进一步阅读 | 文档 | 用途 | @@ -183,4 +152,4 @@ AI 会先判断本次任务是: | [`capture/README.md`](./capture/README.md) | 抓包配置细节(证书安装、代理设置、过滤规则) | # 友情链接 -L站:[Linux do](https://linux.do/) \ No newline at end of file +L站:[Linux do](https://linux.do/) diff --git a/SKILL.md b/SKILL.md index 679fbdb..a40d754 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,15 +1,15 @@ --- name: api-test-E10 -description: 当前 test-automation 项目内置的接口自动化编写 skill,物理位置 `.claude/skills/api-test-E10/`。用于在 `\test-automation\E10自动化\接口自动化测试` 中新增、维护、补齐、迁移接口测试方法与 pytest 用例。触发场景包括:新增接口方法、新增接口测试用例、参考 Java Controller 源码或 Jacoco 报告补齐未覆盖接口、维护已有接口方法与用例、补齐参数化、修复接口断言、按指定位置插入代码、按 URL 查重复实现、处理 UTF-8 中文编码、执行 pytest 并根据真实报错循环修复直到通过。运行时产物统一放在项目根 `api_test_dwp_temp/` 目录下。 +description: 面向 `python + pytest + requests` 接口自动化测试框架的接口自动化编写 skill。用于在用户通过 `config.json` 初始化的项目中新增、维护、补齐、迁移接口测试方法与 pytest 用例。触发场景包括:新增接口方法、新增接口测试用例、参考 Java Controller 源码或 Jacoco 报告补齐未覆盖接口、维护已有接口方法与用例、补齐参数化、修复接口断言、按指定位置插入代码、按 URL 查重复实现、处理 UTF-8 中文编码、执行 pytest 并根据真实报错循环修复直到通过。运行时产物统一放在 `config.json` 的 `paths.runtime_temp_dir` 目录下。 --- # api-test-E10 -随 `test-automation` 项目一起分发的接口自动化编写 skill,把已验证有效的编写习惯、问题处理方式和交付格式沉淀为稳定流程。当前 skill 把“新增”和“维护”拆成两条独立上下文路径,项目根直接由 skill 在 `/.claude/skills/api-test-E10/` 的固定位置推导,运行时产物统一落在项目根的 `api_test_dwp_temp/` 目录下。 +面向 `python + pytest + requests` 接口自动化测试框架的接口自动化编写 skill,把已验证有效的编写习惯、问题处理方式和交付格式沉淀为稳定流程。当前 skill 把“新增”和“维护”拆成两条独立上下文路径,项目根、API 方法目录、pytest 工作目录与运行时产物目录都必须先在 `config.json` 中初始化。 ## 适用范围 -当任务涉及在 `E10自动化/接口自动化测试/` 内**新增或维护**接口测试方法/pytest 用例时,优先使用本 skill。同样适用于按 URL 查重、处理中文编码/导入路径/登录态/返回结构差异、根据真实 pytest 报错循环修复等场景。 +当任务涉及在已初始化的 `python + pytest + requests` 接口自动化项目内**新增或维护**接口测试方法/pytest 用例时,优先使用本 skill。同样适用于按 URL 查重、处理中文编码/导入路径/登录态/返回结构差异、根据真实 pytest 报错循环修复等场景。 ## 🚨 前置门禁(按新增 / 维护分流读取) @@ -74,7 +74,9 @@ Mermaid 源文件与导出 PNG 见 `flow_chart/` 目录,当前覆盖前置 hoo - **抓包底座**:`capture/capture_addon.py` - **索引与匹配工具**:`tools/scan_page_api.py` / `tools/match_captures.py` / `tools/analyze_java_controller.py` / `tools/check_capture_server.py`(入口前置 `tools/preflight_check.py` 由 hook 自动执行,见前置 0) - **全局接口覆盖文档**:`tools/page_api_index.sqlite3` —— 全局 URL 索引,扫描或 AI 新增接口后需更新(纳入版本管理) -- **运行时产物**(项目根 `api_test_dwp_temp/`):`latest.jsonl`(抓包落盘) / `capture_selection.md`(勾选草稿) / `java_sourceCode_analysisResult.md`(Java Controller 源码分析草稿) +- **配置入口**:`config.json` —— 用户必须先配置 `project_root`、`paths.*`、`pytest.*`、`api_index.*` +- **内部接口提取规则**:`tools/api_extract_rules.json` —— 初始化扫描或 AI 维护,用户无需手写正则 +- **运行时产物**(`config.json` 的 `paths.runtime_temp_dir`):`latest.jsonl`(抓包落盘) / `capture_selection.md`(勾选草稿) / `java_sourceCode_analysisResult.md`(Java Controller 源码分析草稿) 如果用户指定了别的参考文件或明确要求"参考当前位置上下文",则以用户要求为先。 diff --git a/capture/README.md b/capture/README.md index 09f37df..7c6f051 100644 --- a/capture/README.md +++ b/capture/README.md @@ -46,8 +46,8 @@ mitmdump -s capture_addon.py --listen-port 12138 Proxy server listening at *:12138 ``` -**如果 `self.baseurl = `**:说明没找到 `E10自动化/接口自动化测试/config.py`,或其 `RunConfig.baseurl` 被注释。请手动打开 config.py 确认当前启用的 baseurl 没被井号注释。 -启动成功后,addon 会通过 `skill_utils/common_function.py` 的通用配置更新方法,把当前解析到的 `RunConfig.baseurl` 同步写入 skill 根目录 `config.json` 的 `baseurl` 字段,便于后续工具读取当前抓包环境。 +**如果 `self.baseurl = `**:说明 `config.json.baseurl` 未配置,或 `project_root` / 路径配置无效。请先完成 skill 根目录 `config.json` 初始化。 +启动成功后,addon 优先使用 `config.json.baseurl` 过滤目标域名。 ## 3. 安装 CA 证书(关键一步) @@ -98,7 +98,7 @@ mitmproxy 需要把根证书装到 Windows 受信任根,才能解密 HTTPS。 2. mitmdump 运行中(步骤 2) 3. 浏览器打开被测环境,完成一次任意业务操作 4. 查看 `api_test_dwp_temp/latest.jsonl`: - - 在项目根目录(含 `E10自动化` 的目录)下执行:`type api_test_dwp_temp\latest.jsonl` + - 在 `config.json.paths.runtime_temp_dir` 对应目录下查看 `latest.jsonl` - 若看到多行 JSON 即成功 每行 JSON 关键字段: @@ -164,8 +164,8 @@ A:`stop.bat` 释放;如占用方不是 mitmdump,查 `netstat -ano | findst **Q:证书装了但仍告警?** A:99% 是装到了"当前用户"而非"本地计算机"。重做步骤 3.2。 -**Q:抓不到 `/oa/second` 下的请求?** -A:打开 `E10自动化/接口自动化测试/config.py`,确认 `RunConfig.baseurl` 匹配你浏览器访问的域名;addon 只记录与 baseurl 匹配的请求。 +**Q:抓不到目标请求?** +A:确认 `config.json.baseurl` 匹配你浏览器访问的域名;addon 只记录与 baseurl 匹配的请求。 **Q:HTTPS 站点访问报错?** A:证书没装好,或代理没开,或 mitmdump 没启动。三者都要满足。 diff --git a/capture/capture_addon.py b/capture/capture_addon.py index f2da2f5..2d8c9e8 100644 --- a/capture/capture_addon.py +++ b/capture/capture_addon.py @@ -34,6 +34,7 @@ from mitmproxy import ctx, http # noqa: E402 from skill_utils.common_function import update_skill_config # noqa: E402 +from skill_utils.config_loader import ConfigError, load_config # noqa: E402 from skill_utils.project_root import ( # noqa: E402 DEFAULT_CONFIG_PATH, TEMP_DIR_NAME, @@ -156,9 +157,17 @@ def _save_baseurl_to_skill_config(baseurl: str) -> None: def _load_baseurl() -> str: + try: + configured = (load_config().raw.get("baseurl") or "").strip() + except ConfigError as e: + ctx.log.warn(f"[api-test-E10] 读取 config.json baseurl 失败: {e}") + configured = "" + if configured: + return configured + repo_root = _resolve_repo_root() if not repo_root: - _warn("未找到项目根(skill 未安装在 /.claude/skills/api-test-E10/ 路径下,或项目根缺少 E10自动化 子目录)") + _warn("未找到项目根,请先在 config.json 中配置 project_root") return "" config_path = os.path.join(repo_root, "E10自动化", "接口自动化测试", "config.py") if not os.path.isfile(config_path): diff --git a/config.json b/config.json index ea9710c..9baf028 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,38 @@ { - "baseurl": "weapp.mulinquan.cn", - "apiDataUpdateDate": "2026-05-21", - "multi_capture": 1 + "framework": "pytest_requests", + "project_root": "D:/AI/AiApiTest-DWP", + "baseurl": "https://www.gbif.org", + "apiDataUpdateDate": "2026-06-10", + "multi_capture": 1, + "paths": { + "api_method_dirs": [ + "test_case/page_api" + ], + "test_case_dirs": [ + "test_case" + ], + "pytest_workdir": ".", + "runtime_temp_dir": "runtime" + }, + "pytest": { + "pythonpath": ".", + "command_template": "pytest {target} -v --tb=short" + }, + "api_index": { + "db_path": "tools/page_api_index.sqlite3", + "extract_rules_path": "tools/api_extract_rules.json", + "scan_dirs": [ + "test_case/page_api" + ], + "include_globs": [ + "**/*.py" + ], + "exclude_globs": [ + "**/__*.py" + ], + "extract_rules": "builtin_requests_plus_generated" + }, + "coding_style": { + "guide_path": "doc/coding_style_guide.md" + } } diff --git a/doc/core_principles.md b/doc/core_principles.md index ccc1836..5c8e8c0 100644 --- a/doc/core_principles.md +++ b/doc/core_principles.md @@ -44,7 +44,7 @@ 1. **扫描新增**:运行 `tools/scan_page_api.py`——库为空时全量重建(id 从 1 起);库非空时全量扫描后按 `Create Date` 取最近 30 天,与现有 `(api_url, method)` 比对,仅追加新接口 2. **强制重建**:`python tools/scan_page_api.py --full` 清空并重写整表,id 重新从 1 编号 -3. **规则扩展**:URL 抽取在 `scan_page_api.py` 的 `URL_EXTRACT_RULES` 追加;HTTP method 抽取在 `REQUEST_METHOD_RULES` 追加(已覆盖 `requests.xxx(...)`、`requests.request("METHOD", ...)`、`self.send_msg("get"/"post", ...)`);URL 抓包匹配在 `skill_utils/api_path_match.py` 追加 +3. **规则扩展**:初始化扫描会生成或维护 `tools/api_extract_rules.json`;URL 抽取优先使用内置 `requests` 规则 + 该规则文件;HTTP method 抽取已覆盖 `requests.xxx(...)`、`requests.request("METHOD", ...)`、`self.send_msg("get"/"post", ...)`;遇到特殊写法时先生成预览,用户确认后再追加规则并增量入库 ## 4. 以真实返回为准 @@ -55,10 +55,10 @@ ## 5. 测试必须闭环 - 完成代码后必须执行 `pytest`,**默认必须跑到新增用例通过才算完成**(除非用户在当前对话中明确强调不需要跑 pytest) -- 执行前先确认工作目录与 `PYTHONPATH`: - - 工作目录:`\test-automation\E10自动化\接口自动化测试\test_case` - - `PYTHONPATH`:`.`(当前目录,即 `test_case`) - - 原因:`conftest.py` 使用 `sys.path.append(os.getcwd())` 动态添加路径,`page_api` 模块位于 `test_case` 目录下 +- 执行前先读取 `config.json` 中的 pytest 配置: + - 工作目录:`paths.pytest_workdir` + - `PYTHONPATH`:`pytest.pythonpath` + - 命令模板:`pytest.command_template` - 记录执行目录、`PYTHONPATH`、执行命令、关键日志、报错信息、最终结果 - 如果失败,必须根据真实报错定位并修复,直到通过 - 如果最终通过依赖特定工作目录或 `PYTHONPATH`,必须明确说明 diff --git a/doc/mode_maintenance_pytest_driven.md b/doc/mode_maintenance_pytest_driven.md index 6c08c2d..fc620e9 100644 --- a/doc/mode_maintenance_pytest_driven.md +++ b/doc/mode_maintenance_pytest_driven.md @@ -22,19 +22,18 @@ 1. **锁定目标用例**:只根据 `[接口用例文件]` 和 `[接口用例位置]` 定位待维护用例,不要求用户补 `[接口方法文件]` / `[接口方法位置]` / `[用例名]`。 2. **读取目标上下文**:读取目标用例全文、所属测试类头部、fixture、相关 `self.xxx` 实例化和导入。 -3. **组装最小 pytest 命令**:优先用目标函数名或用户给出的 pytest `-k` 关键字执行最小范围 pytest;工作目录、`PYTHONPATH` 按 `doc/core_principles.md` 的 pytest 闭环要求处理。 - - **⚠️ PYTHONPATH 设置要求(关键)**: - - **工作目录**:必须切换到 `/E10自动化/接口自动化测试/test_case`(注意是 `test_case` 子目录,不是上层的 `接口自动化测试` 目录) - - **PYTHONPATH**:设置为 `.`(当前目录,即 `test_case`) - - **原因**:`conftest.py` 使用 `sys.path.append(os.getcwd())` 动态添加路径,`page_api` 模块位于 `test_case` 目录下 - - **标准命令格式**: - ```bash - cd "/E10自动化/接口自动化测试/test_case" && PYTHONPATH="." pytest <用例文件路径>::<测试类>::<用例名> -v --tb=short +3. **组装最小 pytest 命令**:优先用目标函数名或用户给出的 pytest `-k` 关键字执行最小范围 pytest;工作目录、`PYTHONPATH` 与命令模板按 `config.json` 的 pytest 配置处理。 + + **⚠️ pytest 配置要求(关键)**: + - **工作目录**:读取 `paths.pytest_workdir` + - **PYTHONPATH**:读取 `pytest.pythonpath` + - **命令模板**:读取 `pytest.command_template`,默认 `pytest {target} -v --tb=short` + - **标准命令格式**(由配置和当前 shell 渲染;Windows/PowerShell 使用 `Set-Location` + `$env:PYTHONPATH`,Bash 使用 `cd` + `PYTHONPATH=...`): + ```powershell + Set-Location -LiteralPath ""; $env:PYTHONPATH=""; pytest -v --tb=short ``` - - **示例**: ```bash - cd "D:/workSpace_001_02/test-automation/E10自动化/接口自动化测试/test_case" && PYTHONPATH="." pytest test_eBuilder_case/test_ebuilder_page_case/test_ebuilder_page_base_api_PC/test_ebuilder_page_api_PC.py::TestEBuilderPageApiPC::test_ebuilder_BAAF_xxx -v --tb=short + cd "" && PYTHONPATH="" pytest -v --tb=short ``` 4. **按真实报错定位**:根据 traceback、断言差异、接口响应、导入错误、fixture 错误或返回层级错误判断维护点。 diff --git a/doc/preflight_gates_maintenance.md b/doc/preflight_gates_maintenance.md index b432287..a8b1546 100644 --- a/doc/preflight_gates_maintenance.md +++ b/doc/preflight_gates_maintenance.md @@ -102,14 +102,22 @@ AI 在 TodoWrite 首项必须显式记录任务类型、方式编号与 2 项必 --- -## 项目根定位说明(本版本无需 AI 写入 config) +## 项目根定位说明(必须先初始化 config) -本 skill 已固定安装在 `/.claude/skills/api-test-E10/` 路径下,项目根由 `skill_utils/project_root.py` 直接从 skill 自身位置推导(`SKILL_ROOT/../../..`)。 +本 skill 面向通用 `python + pytest + requests` 接口自动化项目。正式维护前,用户必须先在 `config.json` 中配置: -因此维护任务信息校验通过后,AI **不再需要**从 `[接口用例文件]` 路径提取并写入 `config.json` 的 `project_path` 字段。抓包与勾选工具会通过 `skill_utils.project_root.resolve_project_root()` 自动定位 `/api_test_dwp_temp/` 落地目录。 +1. `project_root` +2. `paths.api_method_dirs` +3. `paths.test_case_dirs` +4. `paths.pytest_workdir` +5. `paths.runtime_temp_dir` +6. `api_index.scan_dirs` +7. `pytest.pythonpath` / `pytest.command_template` -如果运行时确实出现"找不到项目根"的错误,按以下顺序排查: +维护任务信息校验通过后,AI 不从 `[接口用例文件]` 反推项目根;所有工具统一通过 `skill_utils.project_root.resolve_project_root()` 读取 `config.json.project_root`。 -1. 当前 skill 是否还处在 `/.claude/skills/api-test-E10/` 路径下。 -2. skill 上 3 层目录(即推导出的项目根)下是否存在 `E10自动化` 子目录。 -3. 如以上两点均符合而仍报错,检查文件系统软链 / 符号链接是否被解析错误。 +如果运行时出现"找不到项目根"或"扫描目录不存在",按以下顺序排查: + +1. `config.json.project_root` 是否已填写并指向真实目录。 +2. `api_index.scan_dirs` 是否相对 `project_root` 可解析为真实目录。 +3. `paths.pytest_workdir` 是否相对 `project_root` 可解析为真实目录。 diff --git a/doc/preflight_gates_new.md b/doc/preflight_gates_new.md index 000c338..edecd34 100644 --- a/doc/preflight_gates_new.md +++ b/doc/preflight_gates_new.md @@ -104,14 +104,22 @@ AI 在 TodoWrite 首项必须显式记录任务类型、方式编号与 5 项必 --- -## 项目根定位说明(本版本无需 AI 写入 config) +## 项目根定位说明(必须先初始化 config) -本 skill 已固定安装在 `/.claude/skills/api-test-E10/` 路径下,项目根由 `skill_utils/project_root.py` 直接从 skill 自身位置推导(`SKILL_ROOT/../../..`)。 +本 skill 面向通用 `python + pytest + requests` 接口自动化项目。正式新增前,用户必须先在 `config.json` 中配置: -因此新增任务信息校验通过后,AI **不再需要**从 `[接口用例文件]` 路径提取并写入 `config.json` 的 `project_path` 字段。抓包与勾选工具会通过 `skill_utils.project_root.resolve_project_root()` 自动定位 `/api_test_dwp_temp/` 落地目录。 +1. `project_root` +2. `paths.api_method_dirs` +3. `paths.test_case_dirs` +4. `paths.pytest_workdir` +5. `paths.runtime_temp_dir` +6. `api_index.scan_dirs` +7. `pytest.pythonpath` / `pytest.command_template` -如果运行时确实出现"找不到项目根"的错误,按以下顺序排查: +新增任务信息校验通过后,AI 不从 `[接口用例文件]` 反推项目根;所有工具统一通过 `skill_utils.project_root.resolve_project_root()` 读取 `config.json.project_root`。 -1. 当前 skill 是否还处在 `/.claude/skills/api-test-E10/` 路径下。 -2. skill 上 3 层目录(即推导出的项目根)下是否存在 `E10自动化` 子目录。 -3. 如以上两点均符合而仍报错,检查文件系统软链 / 符号链接是否被解析错误。 +如果运行时出现"找不到项目根"或"扫描目录不存在",按以下顺序排查: + +1. `config.json.project_root` 是否已填写并指向真实目录。 +2. `api_index.scan_dirs` 是否相对 `project_root` 可解析为真实目录。 +3. `paths.pytest_workdir` 是否相对 `project_root` 可解析为真实目录。 diff --git a/skill_utils/__init__.py b/skill_utils/__init__.py index a9a1600..2e56011 100644 --- a/skill_utils/__init__.py +++ b/skill_utils/__init__.py @@ -15,10 +15,13 @@ 公开 API 一览(详见各模块 docstring): - 来自 `project_root`: - REPO_MARKER / TEMP_DIR_NAME / CONFIG_FILENAME + TEMP_DIR_NAME / CONFIG_FILENAME SKILL_ROOT / DEFAULT_CONFIG_PATH / PROJECT_ROOT resolve_project_root / get_temp_dir +- 来自 `config_loader`: + ConfigError / SkillConfig / load_config + - 来自 `common_function`: update_skill_config @@ -45,6 +48,13 @@ get_temp_dir, ) +# --- config_loader ------------------------------------------------------- +from skill_utils.config_loader import ( # noqa: F401 + ConfigError, + SkillConfig, + load_config, +) + # --- common_function ------------------------------------------------------ from skill_utils.common_function import ( # noqa: F401 update_skill_config, @@ -84,6 +94,10 @@ "get_temp_dir", # common_function "update_skill_config", + # config_loader + "ConfigError", + "SkillConfig", + "load_config", # api_index_db "DB_FILENAME", "get_default_db_path", diff --git a/skill_utils/api_path_match.py b/skill_utils/api_path_match.py index e6e7166..3fd7936 100644 --- a/skill_utils/api_path_match.py +++ b/skill_utils/api_path_match.py @@ -32,8 +32,21 @@ def _brace_placeholder_match(covered_path: str, captured_path: str) -> bool: normalized = _normalize(covered_path) if not re.search(r"\{[^/{}]+\}", normalized): return False - pattern = re.escape(normalized) - pattern = re.sub(r"\\\{[^/{}]+\\\}", "[^/]+", pattern) + parts = [] + last = 0 + for match in re.finditer(r"\{[^/{}]+\}", normalized): + parts.append(re.escape(normalized[last:match.start()])) + # 占位符单独作为路径段时,匹配一个非 "/" 段; + # 嵌在路径段内时,兼容历史 `{1}data` 写法: + # `/api/inc/{1}data/x` 可匹配 `/api/inc/foo/data/x` 或 `/api/inc/data/x`。 + full_segment = ( + (match.start() == 0 or normalized[match.start() - 1] == "/") + and (match.end() == len(normalized) or normalized[match.end()] == "/") + ) + parts.append("[^/]+" if full_segment else "(?:[^/]+/)?") + last = match.end() + parts.append(re.escape(normalized[last:])) + pattern = "".join(parts) return re.fullmatch(pattern, _normalize(captured_path)) is not None diff --git a/skill_utils/config_loader.py b/skill_utils/config_loader.py new file mode 100644 index 0000000..f75c66d --- /dev/null +++ b/skill_utils/config_loader.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Author: dengwanpeng + +"""读取通用 pytest + requests 接口自动化项目配置。""" + +import json +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional + + +SKILL_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_CONFIG_PATH = SKILL_ROOT / "config.json" + + +class ConfigError(RuntimeError): + """配置缺失或不合法。""" + + +@dataclass(frozen=True) +class SkillConfig: + raw: Dict[str, Any] + config_path: Path + project_root: Path + api_method_dirs: List[Path] + test_case_dirs: List[Path] + pytest_workdir: Path + runtime_temp_dir: Path + api_scan_dirs: List[Path] + api_index_db_path: Path + extract_rules_path: Path + pytest_pythonpath: str + pytest_command_template: str + + +def _read_json(config_path: Path) -> Dict[str, Any]: + if not config_path.is_file(): + raise ConfigError( + f"未找到 config.json: {config_path}。请先按模板初始化 project_root、paths、pytest 和 api_index 配置。" + ) + try: + data = json.loads(config_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ConfigError(f"config.json 不是合法 JSON: {exc}") from exc + if not isinstance(data, dict): + raise ConfigError("config.json 顶层必须是对象") + return data + + +def _as_dict(data: Dict[str, Any], key: str) -> Dict[str, Any]: + value = data.get(key) or {} + if not isinstance(value, dict): + raise ConfigError(f"config.json 中 {key} 必须是对象") + return value + + +def _as_list(value: Any, key: str) -> List[str]: + if value is None: + return [] + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ConfigError(f"config.json 中 {key} 必须是字符串数组") + return value + + +def _resolve_under(base: Path, value: str) -> Path: + path = Path(value) + if not path.is_absolute(): + path = base / path + return path.resolve() + + +def _resolve_existing_dirs(base: Path, values: List[str], key: str) -> List[Path]: + resolved = [_resolve_under(base, item) for item in values] + missing = [str(path) for path in resolved if not path.is_dir()] + if missing: + raise ConfigError(f"config.json 中 {key} 指向的目录不存在: {', '.join(missing)}") + return resolved + + +def _resolve_skill_file(value: str, default: str) -> Path: + raw = value or default + path = Path(raw) + if not path.is_absolute(): + path = SKILL_ROOT / path + return path.resolve() + + +def load_config(config_path: Optional[str] = None) -> SkillConfig: + """读取并校验 skill 配置。 + + 用户必须配置 project_root。除索引库与内部规则文件外,项目路径均按 + project_root 解析;索引库和规则文件按 skill 根目录解析,保持随 skill 管理。 + """ + cfg_path = Path(config_path).resolve() if config_path else DEFAULT_CONFIG_PATH.resolve() + data = _read_json(cfg_path) + + project_root_raw = data.get("project_root") + if not isinstance(project_root_raw, str) or not project_root_raw.strip(): + raise ConfigError("config.json 缺少必填字段 project_root") + project_root = Path(project_root_raw).expanduser().resolve() + if not project_root.is_dir(): + raise ConfigError(f"config.json 中 project_root 指向的目录不存在: {project_root}") + + paths = _as_dict(data, "paths") + api_index = _as_dict(data, "api_index") + pytest_cfg = _as_dict(data, "pytest") + + api_method_dirs_raw = _as_list(paths.get("api_method_dirs"), "paths.api_method_dirs") + test_case_dirs_raw = _as_list(paths.get("test_case_dirs"), "paths.test_case_dirs") + api_scan_dirs_raw = _as_list(api_index.get("scan_dirs"), "api_index.scan_dirs") + if not api_scan_dirs_raw: + api_scan_dirs_raw = api_method_dirs_raw + if not api_scan_dirs_raw: + raise ConfigError("config.json 缺少 api_index.scan_dirs 或 paths.api_method_dirs") + + api_method_dirs = _resolve_existing_dirs(project_root, api_method_dirs_raw, "paths.api_method_dirs") + test_case_dirs = _resolve_existing_dirs(project_root, test_case_dirs_raw, "paths.test_case_dirs") + api_scan_dirs = _resolve_existing_dirs(project_root, api_scan_dirs_raw, "api_index.scan_dirs") + + pytest_workdir = _resolve_under(project_root, paths.get("pytest_workdir") or ".") + if not pytest_workdir.is_dir(): + raise ConfigError(f"config.json 中 paths.pytest_workdir 指向的目录不存在: {pytest_workdir}") + + runtime_temp_dir = _resolve_under(project_root, paths.get("runtime_temp_dir") or "api_test_dwp_temp") + + return SkillConfig( + raw=data, + config_path=cfg_path, + project_root=project_root, + api_method_dirs=api_method_dirs, + test_case_dirs=test_case_dirs, + pytest_workdir=pytest_workdir, + runtime_temp_dir=runtime_temp_dir, + api_scan_dirs=api_scan_dirs, + api_index_db_path=_resolve_skill_file( + api_index.get("db_path") or "", + "tools/page_api_index.sqlite3", + ), + extract_rules_path=_resolve_skill_file( + api_index.get("extract_rules_path") or "", + "tools/api_extract_rules.json", + ), + pytest_pythonpath=str(pytest_cfg.get("pythonpath") or "."), + pytest_command_template=str(pytest_cfg.get("command_template") or "pytest {target} -v --tb=short"), + ) + + +def get_project_root(config_path: Optional[str] = None) -> str: + return str(load_config(config_path).project_root) + + +def get_runtime_temp_dir(config_path: Optional[str] = None, create: bool = False) -> str: + temp_dir = load_config(config_path).runtime_temp_dir + if create: + os.makedirs(temp_dir, exist_ok=True) + return str(temp_dir) diff --git a/skill_utils/project_root.py b/skill_utils/project_root.py index 52e5e40..9befd3c 100644 --- a/skill_utils/project_root.py +++ b/skill_utils/project_root.py @@ -5,9 +5,9 @@ """项目根定位与运行时产物目录解析(多模块共用)。 设计要点: -- 本 skill 是「项目内 skill」,物理位置为 `/.claude/skills/api-test-E10/`。 -- 项目根直接由 skill 自身位置推导:`SKILL_ROOT/../../..`。 -- 不再依赖 CWD 向上搜索、也不再依赖 config.json 的 project_path 字段。 +- 本 skill 面向通用 python + pytest + requests 接口自动化项目。 +- 项目根必须由 skill 根目录 config.json 的 project_root 明确配置。 +- 不再依赖 CWD 向上搜索,也不再使用 E10 目录 marker fallback。 - 仍提供 on_warn/on_info callback 注入,便于 mitmdump / 独立脚本各自适配日志方式。 被以下模块共用: @@ -20,10 +20,12 @@ import os from typing import Callable, Optional +from skill_utils.config_loader import ConfigError, load_config -# 仓库结构硬约束:test-automation 项目根下必有 "E10自动化" 子目录 -REPO_MARKER = "E10自动化" -# skill 内运行时产物在消费方项目下的子目录名(保持不变以兼容现有 .gitignore 与历史落点) + +# 旧版常量仅保留给历史 import 兼容;不再作为项目根定位依据。 +REPO_MARKER = "" +# 运行时产物默认落在消费方项目下的子目录名。 TEMP_DIR_NAME = "api_test_dwp_temp" # config.json 在 skill 根目录(baseurl / apiDataUpdateDate 等运行时配置仍写入此处) CONFIG_FILENAME = "config.json" @@ -32,8 +34,8 @@ SKILL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEFAULT_CONFIG_PATH = os.path.join(SKILL_ROOT, CONFIG_FILENAME) -# 项目根:skill 位于 /.claude/skills/api-test-E10,向上 3 层即项目根 -PROJECT_ROOT = os.path.normpath(os.path.join(SKILL_ROOT, "..", "..", "..")) +# 旧版 PROJECT_ROOT 不再用于定位;保留空字符串兼容历史 import。 +PROJECT_ROOT = "" LogFn = Optional[Callable[[str], None]] @@ -47,21 +49,17 @@ def resolve_project_root( on_warn: LogFn = None, on_info: LogFn = None, ) -> Optional[str]: - """返回项目根绝对路径。 - - 通过 skill 自身在 /.claude/skills/api-test-E10/ 的固定位置推导项目根。 - 校验:项目根下必须存在 REPO_MARKER 子目录(防御 skill 被复制到错误位置)。 - """ + """返回 config.json 中声明的项目根绝对路径。""" warn = on_warn or _noop info = on_info or _noop - if not os.path.isdir(os.path.join(PROJECT_ROOT, REPO_MARKER)): - warn( - f"未在推导出的项目根下找到 {REPO_MARKER} 子目录: {PROJECT_ROOT}。" - f"请确认 skill 安装在 /.claude/skills/api-test-E10/ 路径下。" - ) + try: + config = load_config() + except ConfigError as exc: + warn(f"项目根配置无效: {exc}") return None - info(f"使用项目根 {PROJECT_ROOT}") - return PROJECT_ROOT + project_root = str(config.project_root) + info(f"使用项目根 {project_root}") + return project_root def get_temp_dir( @@ -69,9 +67,14 @@ def get_temp_dir( on_info: LogFn = None, ) -> Optional[str]: """返回 /api_test_dwp_temp 目录绝对路径,并确保其存在。""" - repo_root = resolve_project_root(on_warn=on_warn, on_info=on_info) - if not repo_root: + warn = on_warn or _noop + info = on_info or _noop + try: + config = load_config() + except ConfigError as exc: + warn(f"运行时目录配置无效: {exc}") return None - temp_dir = os.path.join(repo_root, TEMP_DIR_NAME) + temp_dir = str(config.runtime_temp_dir) os.makedirs(temp_dir, exist_ok=True) + info(f"使用运行时目录 {temp_dir}") return temp_dir diff --git a/skill_utils/pytest_command.py b/skill_utils/pytest_command.py new file mode 100644 index 0000000..34e0877 --- /dev/null +++ b/skill_utils/pytest_command.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Author: dengwanpeng + +"""按 config.json 构造 pytest 执行命令。""" + +import os +from typing import Optional + +from skill_utils.config_loader import load_config + + +def _powershell_quote(value: object) -> str: + return str(value).replace('"', '`"') + + +def build_pytest_command(target: str, config_path: Optional[str] = None, shell: Optional[str] = None) -> str: + config = load_config(config_path) + pytest_cmd = config.pytest_command_template.format(target=target) + shell_name = (shell or ("powershell" if os.name == "nt" else "bash")).lower() + if shell_name in {"powershell", "pwsh"}: + workdir = _powershell_quote(config.pytest_workdir) + pythonpath = _powershell_quote(config.pytest_pythonpath) + return f'Set-Location -LiteralPath "{workdir}"; $env:PYTHONPATH="{pythonpath}"; {pytest_cmd}' + if shell_name == "bash": + return ( + f'cd "{config.pytest_workdir}" && ' + f'PYTHONPATH="{config.pytest_pythonpath}" ' + f"{pytest_cmd}" + ) + raise ValueError(f"不支持的 shell: {shell}") diff --git a/tools/api_extract_rules.json b/tools/api_extract_rules.json new file mode 100644 index 0000000..eb8cb60 --- /dev/null +++ b/tools/api_extract_rules.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "generated_at": "2026-06-10 17:28:24", + "url_extract_rules": [], + "method_extract_rules": [] +} \ No newline at end of file diff --git a/tools/append_extract_rule.py b/tools/append_extract_rule.py new file mode 100644 index 0000000..4fb7f1d --- /dev/null +++ b/tools/append_extract_rule.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# Author: dengwanpeng + +"""追加项目特定接口提取规则,并按预览确认后增量写入索引。""" + +import argparse +import json +import os +import sys +from pathlib import Path +from typing import Dict, List + + +TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) +_SKILL_ROOT = os.path.dirname(TOOLS_DIR) +if _SKILL_ROOT not in sys.path: + sys.path.insert(0, _SKILL_ROOT) + +from skill_utils.api_index_db import existing_url_method_pairs, insert_methods # noqa: E402 +from tools import scan_page_api # noqa: E402 + + +def _load_rules(path: str) -> Dict[str, List[dict]]: + rules_path = Path(path) + if not rules_path.is_file(): + return {"url_extract_rules": [], "method_extract_rules": []} + try: + data = json.loads(rules_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {"url_extract_rules": [], "method_extract_rules": []} + if not isinstance(data, dict): + return {"url_extract_rules": [], "method_extract_rules": []} + return { + "url_extract_rules": list(data.get("url_extract_rules") or []), + "method_extract_rules": list(data.get("method_extract_rules") or []), + } + + +def _write_rules(path: str, rules: Dict[str, List[dict]]) -> None: + rules_path = Path(path) + rules_path.parent.mkdir(parents=True, exist_ok=True) + rules_path.write_text(json.dumps(rules, ensure_ascii=False, indent=2), encoding="utf-8") + + +def _merge_rules(current: Dict[str, List[dict]], update: Dict[str, List[dict]]) -> Dict[str, List[dict]]: + merged = { + "url_extract_rules": list(current.get("url_extract_rules") or []), + "method_extract_rules": list(current.get("method_extract_rules") or []), + } + for key in ("url_extract_rules", "method_extract_rules"): + seen = {(item.get("name"), item.get("pattern")) for item in merged[key] if isinstance(item, dict)} + for item in update.get(key) or []: + if not isinstance(item, dict): + continue + marker = (item.get("name"), item.get("pattern")) + if marker in seen: + continue + merged[key].append(item) + seen.add(marker) + return merged + + +def append_extract_rule( + repo_root: str, + scan_dirs: List[str], + db_path: str, + rules_path: str, + rule_update: Dict[str, List[dict]], + apply: bool = False, +) -> Dict: + current_rules = _load_rules(rules_path) + merged_rules = _merge_rules(current_rules, rule_update) + temp_rules_path = Path(rules_path).with_suffix(".preview.json") + _write_rules(str(temp_rules_path), merged_rules) + try: + url_rules, method_rules = scan_page_api._load_extract_rules(str(temp_rules_path)) + records, scanned_files = scan_page_api._scan_all( + repo_root, + scan_dirs, + url_rules=url_rules, + method_rules=method_rules, + ) + finally: + try: + temp_rules_path.unlink() + except OSError: + pass + + existing_pairs = existing_url_method_pairs(db_path) + new_records = scan_page_api._filter_truly_new(records, existing_pairs) + result = { + "scanned_files": scanned_files, + "new_records": len(new_records), + "records": new_records, + "inserted": 0, + } + if apply: + _write_rules(rules_path, merged_rules) + result["inserted"] = insert_methods(db_path, new_records) + return result + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo-root", required=True) + parser.add_argument("--scan-dir", action="append", required=True) + parser.add_argument("--db", required=True) + parser.add_argument("--rules", required=True) + parser.add_argument("--rule-update", required=True, help="规则 JSON 文件") + parser.add_argument("--apply", action="store_true") + args = parser.parse_args() + update = json.loads(Path(args.rule_update).read_text(encoding="utf-8")) + result = append_extract_rule( + repo_root=args.repo_root, + scan_dirs=args.scan_dir, + db_path=args.db, + rules_path=args.rules, + rule_update=update, + apply=args.apply, + ) + print(json.dumps(result, ensure_ascii=False, indent=2, default=str)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/init_project_scan.py b/tools/init_project_scan.py new file mode 100644 index 0000000..57c8e8b --- /dev/null +++ b/tools/init_project_scan.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Author: dengwanpeng + +"""项目初始化扫描:生成编码风格草稿、内部提取规则与接口索引。""" + +import argparse +import json +import os +import sys +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional + + +TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) +_SKILL_ROOT = os.path.dirname(TOOLS_DIR) +if _SKILL_ROOT not in sys.path: + sys.path.insert(0, _SKILL_ROOT) + +from skill_utils.api_index_db import replace_index # noqa: E402 +from skill_utils.config_loader import load_config # noqa: E402 +from tools import scan_page_api # noqa: E402 + + +DEFAULT_RULES = { + "version": 1, + "generated_at": "", + "url_extract_rules": [], + "method_extract_rules": [], +} + + +def _read_text(path: str) -> str: + return Path(path).read_text(encoding="utf-8") + + +def _write_json(path: Path, data: Dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +def _render_coding_style_draft(api_template: str, case_template: str) -> str: + api_source = _read_text(api_template) + case_source = _read_text(case_template) + return "\n".join( + [ + "# 接口编码风格指南草稿", + "", + "> 本文件由初始化扫描生成,请人工确认后再合并到 doc/coding_style_guide.md。", + "", + "## 接口方法模板", + "", + "```python", + api_source, + "```", + "", + "## pytest 用例模板", + "", + "```python", + case_source, + "```", + "", + ] + ) + + +def _write_rules(path: Path) -> None: + data = dict(DEFAULT_RULES) + data["generated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + _write_json(path, data) + + +def init_project_scan( + config_path: Optional[str] = None, + api_template: Optional[str] = None, + case_template: Optional[str] = None, + draft_path: Optional[str] = None, +) -> Dict[str, int]: + config = load_config(config_path) + if not api_template or not case_template: + raise ValueError("必须提供 api_template 和 case_template") + + draft = Path(draft_path) if draft_path else config.runtime_temp_dir / "coding_style_guide_draft.md" + draft.parent.mkdir(parents=True, exist_ok=True) + draft.write_text(_render_coding_style_draft(api_template, case_template), encoding="utf-8") + + _write_rules(config.extract_rules_path) + url_rules, method_rules = scan_page_api._load_extract_rules(str(config.extract_rules_path)) + records, scanned_files = scan_page_api._scan_all( + str(config.project_root), + [str(path) for path in config.api_scan_dirs], + url_rules=url_rules, + method_rules=method_rules, + ) + metadata = { + "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "repo_root": str(config.project_root).replace("\\", "/"), + "scan_mode": "init_project_scan", + "scanned_files": str(scanned_files), + "total_methods": str(len(records)), + } + replace_index(str(config.api_index_db_path), records, metadata) + return {"records": len(records), "scanned_files": scanned_files} + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--config", default=None, help="config.json 路径") + parser.add_argument("--api-template", required=True, help="接口方法模板文件") + parser.add_argument("--case-template", required=True, help="pytest 用例模板文件") + parser.add_argument("--draft-out", default=None, help="编码风格草稿输出路径") + args = parser.parse_args() + result = init_project_scan( + config_path=args.config, + api_template=args.api_template, + case_template=args.case_template, + draft_path=args.draft_out, + ) + print(f"[init_project_scan] records={result['records']} scanned_files={result['scanned_files']}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/page_api_index.sqlite3 b/tools/page_api_index.sqlite3 index 5615099b95321fa68a12472e822afc33e5576988..bc4ea289cdd5ff0b14083e95a0bc83821334f799 100644 GIT binary patch delta 7198 zcmbuDX>b$g8OK*%S+->Dz8FzRAc--?F^*$oSpsu}B*vsJB-juU9B_=c8k0hgmGqy$3BVPcMio3?3FJ~Wd~r!$>N`hg(Jp6QJHDKq)dnf`aBUCFW} zU!M8N(tAAb^WWz^o_*UMl(s!6?V805Q55wEXu!k6^U4%MS2ur5bYwb3z9qNGF=8X@ z$#SA6)BAE~KVrKdKW8$}$fR@fyjIR>akv_Io73vy9lX=WHD_ng`Pti@wr$&ai<|Q` zdJU$jD!L%s=R&2$!TTCr4JgY_rwfSC-^n*vw%D3^Z)x|!oML|-O&1#6+*aOe;oLTh z$K~?*c(2c5<-ELJC>M%z(&!wchj+Wg`V~v`&xtJ(dYTI=>oAxb~#-~7ez`umUnrN*XD96r_zOr;R0yEmh5gCq4U+TzA>@}Janx8!2aOn6QPa^ zq4S-g6MJeR2AooNBPR=QSJ@Z^h*fh(QC z_MU+=pG;8HMOl+xOg5GsUWe&OI=~TlhSy4 zJQ^i`0*zUOvKLp&akM9n3;Ws#~d}JAVrM*S0LENmoMXHL`CT}yr{kK8~d;5>}_IF+$Jh^XT@^;AT zk~?_Q_1h`qYYI90;sc5z1_ciZLe@ zNX1 zsy8_YFKdaGTbx39LPh-@ZT+V@p4{&Vc1`LeTa{8*)Rij1lR&F-3RSx_K+EdF`(L8f zD0XcM%M<#B(-R?Giij4)CDayH6-`C%9|@lPu(ZT9yUI1Y!sYZK@)J%+OykfSSWFYD zGQUDrTbVSLVxZi-QAoj(3q!0z=X)o%_svV`xG;VoH;qt+f@QM1hGFiCE=pk#e}o^t zxJ5tH8TM^53B$}09*Uy`<|+tfER4Ek9Ojmn=RDR%$*=IQ{sSGsy?uj6E+b9r?>ZH_ z_ED%qC*-S%9$nq2DJVO_vG6d$s{pGxNa0228k5PikZIZ;<~#ON_GPUpYh%_|n*V4H z_RW2@NwcU3jdR2cB1v_c9>Q%KxV}Cl*_?B>HB^uonZ?s+G zR&f<(ML2|s_N#D;6O%niQ!+t@BaZP-+!#C5E)`dvYKRdBxHN=ZA?Fz42y5d;aAr2B zY8Ip%A?84pjmMOUW>?3Vt%9#-C4DkG}P>CfX5h zB`wGl8-nzP&Wxp#KXRQRzXWTIw0+D&MfRSoBD!|0eKhlFF5Fg(m4EoHO5|lIW~WdO zZy(oOUffdJo>@rO#F&q79gXkYw7Z% zNMv+)V`VN~=}&H1n8WmR+AzF@IJ48H)5b&-I6UqA6nR8~>xT)a)(6W8*z-ON9{6lf)*=vHF2Z(hutjdV_zLJ zij+EYT=U_rVm`oBoFVQ%vb6vQ;r}`+L=n7VMMF^`+ zEzLA(z!zdtM&=ue{EJ*A9fTu`Nj5vkK4g!x+t`(CF7qSvP2b$MON>%jy5jacEq;4W zTEir*(I&0QN?M~yTBA-{lbN(e+=>$FQ6=q@p0pS7Kq)W+6IcKif-;k_4!qaX8 z0q{Q9EtKrRm%U&g*bmx42lxQ|5*z@X;2<~zy1-%ZE6@#&fTQ3T=mEb5$H5735}X3R z0Uv_jg45s(_z3(?@aQb~7@Py=!6)DX_!RseTm+ZEWpD-bf~(*&&t;62%h~DKE4Eh27duxfxm)B;70d>uBUtcy+}J}cSZO9if#o+ Z12j;Abf5wmAQPy824vl==+@rV{vRV_mrDQu delta 1879 zcmY+E3s4hB7{~8&$>r{nyETOPLP+ExQiOznprC>%icheSlL+l-$8E%sGfxzfFC!*p8->24q#*8FU-%N2Mqe^+uBNL}?HAcAQhR_LvNZ z8k+C-1=G_rvfNe!muL-^dMheapJ#4axf*m2%+$*4DG?($E@^4NJ5LRIyn!-LrQc7Q zrb$`lI?g_^QVsa26B?fFT;<%pL66Wc=ps6a4x*jtBeX1RDK0_U zYG+)Pl}lz{r&;M)qE-gs!Fmgq_{MvaxU+;p<#GM|H8Tg(PWIFyW7i1c~V+Wav+h>7H!L1y~WPEc8$RvET0AwP* zJ_Y1$d`$tFfUh_}itz!|@)v?lAmG21q{MJ{e>v*2aS5;mt81xp-3{$Pipd zhYZGR%RvU=TFAbN$@!aZF#BZyIPk@I;DF~nfLYIII+>5tfc-m@fEgV$rSw1Oc=!Ec zVA@?P&~=Aq4 z5HS8Y-Fn=&bAhqP=;YpEI>8a{0<<6EfiaE!fYAq+1AFbKe$-dg@40U_u*a7Rf!!PE zd+D}^KBw!aE}(5EZAb1%1zHJBH)1PI)RL{ym))g)84%Uy1C=_uS9$$>pn2W_o{m6_7@q z(L#3r%0(!QR#;B!!=eWadI0C+HriBd%(Adt97m7o!q;b;scEJQ%JOC)^D(*#Cn|`t z5UZS5_9`osS&CKelz$AzmuNB>|Aiq!3XOt++wI&?opv(4C(rJME+Nf+F-G#1Kzmz( z9-BzocP1NYEf%|xTkC|bw8~q1aXDn@c2jpTb{%xLByi&hntG>)+ z@awa5eugnV@ST}E_2WcZ|X%X!Fpv%zSKpI57XcP9g^jp$wlDie>@))x-3*D&m}Sz3}$I;SunJMX0(dL z%o3u>0*h#;<7$)iwlSnNSAa6+vq+Nfg1ItIZ_6c*9fFm#Is}7xali+6WpE?>RYMk8 zgy?NiIupr_*71DLIbdSKa)Nbhs{kKGhF&if(_>rh)LW+$$75p;f^rNNsk{`-s${)& z0%>bCMv_bMrU>5B2Ih)woyHh)yh9hsyBeVXq4qKmOD=>BVjtfHTB9hQUQx_)v={YP zzEQ@=9r6x2-+bR(Z+4n~F!{pq)dytaSimFVdMFyj)WCZ-JxowZQyxFV@B*4X;$nj&Zi$a zVrm)Xq`c9bCv}Ruge}HX{4+y~{s?!AV{{`}4^yk9?e=g}S0-yMS~QV23oc+%>Mwy!Tp~A diff --git a/tools/scan_page_api.py b/tools/scan_page_api.py index 7439d96..05e6082 100644 --- a/tools/scan_page_api.py +++ b/tools/scan_page_api.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # Author: dengwanpeng -"""扫描 E10自动化/接口自动化测试/test_case/page_api/ 下所有 *.py。 +"""扫描 config.json 中 api_index.scan_dirs 下所有 *.py。 -提取 page_api 中已覆盖接口,写入 tools/page_api_index.sqlite3。 +提取已覆盖接口,写入 tools/page_api_index.sqlite3。 用法: python scan_page_api.py # 自动模式:空库走全量替换;非空库走增量追加 @@ -13,13 +13,15 @@ api_url、api_name、api_desc、author、create_date、update_date、method 扫描规则维护: - 1. URL 抽取规则:在本文件的 URL_EXTRACT_RULES 中追加。 - 2. HTTP method 抽取规则:在 REQUEST_METHOD_RULES 中追加。 - 3. 跨脚本复用的基础能力请放到 skill_utils/ 下。 + 1. 内置 URL 抽取规则在 URL_EXTRACT_RULES。 + 2. 内置 HTTP method 抽取规则在 REQUEST_METHOD_RULES。 + 3. 项目特定规则由初始化扫描生成到 tools/api_extract_rules.json。 + 4. 跨脚本复用的基础能力请放到 skill_utils/ 下。 """ import argparse import ast +import json import os import re import sys @@ -50,6 +52,7 @@ replace_index, ) from skill_utils.common_function import update_skill_config # noqa: E402 +from skill_utils.config_loader import ConfigError, load_config # noqa: E402 from skill_utils.project_root import resolve_project_root # noqa: E402 @@ -71,6 +74,24 @@ def _resolve_repo_root() -> Optional[str]: return resolve_project_root(on_warn=_warn) +def _resolve_scan_roots() -> Tuple[Optional[str], List[str]]: + try: + config = load_config() + except ConfigError as exc: + _warn(f"读取扫描配置失败: {exc}") + return None, [] + return str(config.project_root), [str(path) for path in config.api_scan_dirs] + + +def _resolve_extract_rules_path() -> Optional[str]: + try: + config = load_config() + except ConfigError as exc: + _warn(f"读取提取规则配置失败: {exc}") + return None + return str(config.extract_rules_path) + + URL_EXTRACT_RULES = [ { "name": "quoted_http_url", @@ -107,6 +128,10 @@ def _resolve_repo_root() -> Optional[str]: re.compile(r"requests\.request\(\s*['\"]([A-Za-z]+)['\"]"), # requests.get(...) / requests.post(...) / 同名快捷方法 re.compile(r"requests\.(get|post|put|delete|patch|head|options)\(", re.IGNORECASE), + # BaseAPI 封装常见写法:self.get(url, ...) / self.post(url, ...) + re.compile(r"\bself\.(get|post|put|delete|patch|head|options)\(", re.IGNORECASE), + # BaseAPI 通用写法:self.request("GET", url, ...) + re.compile(r"\bself\.request\(\s*['\"]([A-Za-z]+)['\"]", re.IGNORECASE), # self.send_msg("post", url, ...) / self.xxx.send_msg('get', url, ...) re.compile(r"\.send_msg\(\s*['\"]([A-Za-z]+)['\"]"), ] @@ -115,9 +140,51 @@ def _resolve_repo_root() -> Optional[str]: DATE_PREFIX_RE = re.compile(r"^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})") -def _extract_urls_from_source(source: str) -> List[str]: +def _compile_rule(rule: dict) -> Optional[dict]: + try: + compiled = re.compile(rule["pattern"], re.IGNORECASE if rule.get("ignore_case", True) else 0) + except Exception as exc: + _warn(f"提取规则编译失败 {rule.get('name') or ''}: {exc}") + return None + return { + "name": rule.get("name") or "generated_rule", + "pattern": compiled, + "group": int(rule.get("group") or 1), + } + + +def _load_extract_rules(rules_path: Optional[str] = None): + url_rules = list(URL_EXTRACT_RULES) + method_rules = list(REQUEST_METHOD_RULES) + if not rules_path or not os.path.isfile(rules_path): + return url_rules, method_rules + try: + with open(rules_path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as exc: + _warn(f"读取接口提取规则失败: {exc}") + return url_rules, method_rules + if not isinstance(data, dict): + _warn("接口提取规则文件顶层必须是对象,已忽略") + return url_rules, method_rules + for rule in data.get("url_extract_rules") or []: + if not isinstance(rule, dict): + continue + compiled = _compile_rule(rule) + if compiled: + url_rules.append(compiled) + for rule in data.get("method_extract_rules") or []: + if not isinstance(rule, dict): + continue + compiled = _compile_rule(rule) + if compiled: + method_rules.append(compiled["pattern"]) + return url_rules, method_rules + + +def _extract_urls_from_source(source: str, url_rules=None) -> List[str]: urls: List[str] = [] - for rule in URL_EXTRACT_RULES: + for rule in url_rules or URL_EXTRACT_RULES: for match in rule["pattern"].finditer(source): urls.append(_clean_url_path(match.group(rule["group"]))) return [url for url in urls if url] @@ -186,8 +253,8 @@ def _extract_comment_meta(body_text: str) -> Dict[str, str]: return meta -def _extract_http_method(body_text: str) -> str: - for rule in REQUEST_METHOD_RULES: +def _extract_http_method(body_text: str, method_rules=None) -> str: + for rule in method_rules or REQUEST_METHOD_RULES: match = rule.search(body_text) if match: return match.group(1).upper() @@ -213,7 +280,7 @@ def _parse_create_date(value: str) -> Optional[date]: return None -def _parse_file(path: str) -> List[dict]: +def _parse_file(path: str, url_rules=None, method_rules=None) -> List[dict]: try: with open(path, "r", encoding="utf-8") as f: source = f.read() @@ -239,12 +306,12 @@ def _parse_file(path: str) -> List[dict]: start = sub.lineno - 1 end = getattr(sub, "end_lineno", sub.lineno) or sub.lineno body_text = "\n".join(src_lines[start:end]) - urls = _unique_keep_order(_extract_urls_from_source(body_text)) + urls = _unique_keep_order(_extract_urls_from_source(body_text, url_rules=url_rules)) if not urls: continue meta = _extract_comment_meta(body_text) api_desc = _extract_doc_desc(sub) - http_method = _extract_http_method(body_text) + http_method = _extract_http_method(body_text, method_rules=method_rules) for url in urls: results.append({ "class": cls_name, @@ -271,22 +338,24 @@ def _iter_api_files(root: str): yield os.path.join(dirpath, filename) -def _scan_all(repo_root: str, pages_api_root: str) -> Tuple[List[dict], int]: - """全量扫描 page_api 目录,返回 (records, scanned_files)。""" +def _scan_all(repo_root: str, pages_api_root, url_rules=None, method_rules=None) -> Tuple[List[dict], int]: + """全量扫描配置的 API 目录,返回 (records, scanned_files)。""" records: List[dict] = [] scanned_files = 0 - for fp in _iter_api_files(pages_api_root): - rel = os.path.relpath(fp, repo_root).replace("\\", "/") - try: - mtime = int(os.path.getmtime(fp)) - except OSError: - continue - scanned_files += 1 - items = _parse_file(fp) - for item in items: - item["file"] = rel - item["mtime"] = mtime - records.append(item) + roots = pages_api_root if isinstance(pages_api_root, (list, tuple)) else [pages_api_root] + for root in roots: + for fp in _iter_api_files(root): + rel = os.path.relpath(fp, repo_root).replace("\\", "/") + try: + mtime = int(os.path.getmtime(fp)) + except OSError: + continue + scanned_files += 1 + items = _parse_file(fp, url_rules=url_rules, method_rules=method_rules) + for item in items: + item["file"] = rel + item["mtime"] = mtime + records.append(item) return records, scanned_files @@ -325,7 +394,7 @@ def _filter_truly_new( def _build_metadata( repo_root: str, - pages_api_root: str, + pages_api_root, scanned_files: int, total_methods: int, unique_paths: int, @@ -334,7 +403,10 @@ def _build_metadata( return { "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "repo_root": repo_root.replace("\\", "/"), - "pages_api_root": os.path.relpath(pages_api_root, repo_root).replace("\\", "/"), + "pages_api_root": ";".join( + os.path.relpath(path, repo_root).replace("\\", "/") + for path in (pages_api_root if isinstance(pages_api_root, (list, tuple)) else [pages_api_root]) + ), "scanner_version": SCANNER_VERSION, "scanned_files": str(scanned_files), "total_methods": str(total_methods), @@ -378,29 +450,32 @@ def main(): parser.add_argument("--db", default=INDEX_DB_PATH, help="SQLite 索引路径(默认 tools/page_api_index.sqlite3)") args = parser.parse_args() - repo_root = _resolve_repo_root() + repo_root, pages_api_roots = _resolve_scan_roots() if not repo_root: print( - "ERROR: 未找到项目根(含 E10自动化 目录)。" - "请确认 skill 安装在 /.claude/skills/api-test-E10/ 路径下。", + "ERROR: 未找到项目根。请先在 config.json 中配置 project_root 与 api_index.scan_dirs。", file=sys.stderr, ) return 1 - pages_api_root = os.path.join( - repo_root, "E10自动化", "接口自动化测试", "test_case", "page_api" - ) - if not os.path.isdir(pages_api_root): - print(f"ERROR: 未找到 page_api 目录 {pages_api_root}", file=sys.stderr) + missing_scan_roots = [path for path in pages_api_roots if not os.path.isdir(path)] + if missing_scan_roots: + print(f"ERROR: 未找到 API 扫描目录 {missing_scan_roots}", file=sys.stderr) return 1 force_full = args.full or is_empty(args.db) - all_records, scanned_files = _scan_all(repo_root, pages_api_root) + url_rules, method_rules = _load_extract_rules(_resolve_extract_rules_path()) + all_records, scanned_files = _scan_all( + repo_root, + pages_api_roots, + url_rules=url_rules, + method_rules=method_rules, + ) if force_full: unique_paths = len({item.get("api_url") for item in all_records if item.get("api_url")}) metadata = _build_metadata( - repo_root, pages_api_root, scanned_files, len(all_records), unique_paths, + repo_root, pages_api_roots, scanned_files, len(all_records), unique_paths, mode="full", ) replace_index(args.db, all_records, metadata) @@ -420,7 +495,7 @@ def main(): metadata = _build_metadata( repo_root, - pages_api_root, + pages_api_roots, scanned_files, total_methods=len(all_records), unique_paths=len({item.get("api_url") for item in all_records if item.get("api_url")}), From 2b67ae37c48fa282707ee876e463e6ac7bf9dcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=93=E4=B8=87=E9=B9=8F?= Date: Tue, 23 Jun 2026 17:25:38 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - ...6\207\346\241\243-deepseek\347\211\210.md" | 816 ------------------ doc/mode_java_controller_source.md | 3 +- 3 files changed, 2 insertions(+), 818 deletions(-) delete mode 100644 "api-test-E10\347\232\204\350\257\246\347\273\206\344\275\277\347\224\250\350\257\264\346\230\216\346\226\207\346\241\243-deepseek\347\211\210.md" diff --git a/.gitignore b/.gitignore index 9c07328..9741c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ __pycache__/ # 临时文件 temp_*.py -/tests/ /api_test_dwp_temp/ # 旧版运行时产物(残留) diff --git "a/api-test-E10\347\232\204\350\257\246\347\273\206\344\275\277\347\224\250\350\257\264\346\230\216\346\226\207\346\241\243-deepseek\347\211\210.md" "b/api-test-E10\347\232\204\350\257\246\347\273\206\344\275\277\347\224\250\350\257\264\346\230\216\346\226\207\346\241\243-deepseek\347\211\210.md" deleted file mode 100644 index fb7713c..0000000 --- "a/api-test-E10\347\232\204\350\257\246\347\273\206\344\275\277\347\224\250\350\257\264\346\230\216\346\226\207\346\241\243-deepseek\347\211\210.md" +++ /dev/null @@ -1,816 +0,0 @@ -# 一、使用前准备 -## claude code安全提醒 - -![](https://cdn.nlark.com/yuque/0/2026/png/29548917/1778233651697-6e5eeda9-9b4b-4ee0-bcff-1219ab4555cb.png) - -**claude官方提醒** - -1. Claude可能会犯错误,您应该经常检查Claude的响应,特别是在运行代码时。 - -2. 由于提示注入的风险,只对你信任的代码使用它更多详细信息请参见:[https://code.claude.com/docs/en/security](https://code.claude.com/docs/en/security) - -## 开源包被投毒的风险案例 -1. LiteLLM投毒(我们公司遇到的安全事故) - -[https://mp.weixin.qq.com/s/gVxO9vNYu1gNvHnD9mPFsg](https://mp.weixin.qq.com/s/gVxO9vNYu1gNvHnD9mPFsg) - -2. axios投毒(我朋友电脑使用龙虾安装环境中招,导致需要整机重装系统) - -[https://mp.weixin.qq.com/s/IBf3K2pbP9cKrJjpzddynA](https://mp.weixin.qq.com/s/IBf3K2pbP9cKrJjpzddynA) -- 这两个案例都是黑客向使用量很高的最新版本的开源包投毒。而使用AI帮我们安装环境时,AI都会默认安装最新版本的依赖包。这就导致使用AI遇到这种风险的概率更高,这也提醒我们使用AI需要谨慎。 - -## 适用环境 -| | 建议要求 | -| --- | --- | -| 操作系统 | Windows 10 / Windows 11 | -| IDE | PyCharm | -| Python | 3.8 及以上 | -| 账号 | DeepSeek 开发者账号 | -| 工具 | Claude Code、CC Switch、PyCharm CC GUI 插件 | -| Skill | `api-test-E10`;维护方式 4 依赖 `/test-fixing` 和 `/Debugging` | - - -## 需要提前准备的信息 -请先准备以下内容,后续步骤会用到: - -+ DeepSeek 开发者账号。 -+ DeepSeek API Key。 -+ `api-test-E10` Skill 所在路径,例如:`C:\Users\admin\.claude\skills\api-test-E10`。 -+ 第三方依赖 Skill:`/test-fixing`、`/Debugging`。其中 `/test-fixing` 是维护方式 4 的默认修复流程,`/Debugging` 只在测试修复无法解决或调用栈/前后接口信息不明确时使用。 - -# 二、工具和账号配置 -## 1. 安装 Claude Code(Windows) -### 1.1 安装前检查 Node.js,版本需要在22以上 -1. 打开 PowerShell。 -2. 执行: - -```powershell -node -v -``` - -3. 如果能看到版本号,例如 `v20.x.x`,说明 Node.js 已安装。 -4. 如果提示未识别 `node` 命令,请先安装 Node.js: - - 下载地址:`https://nodejs.org/` - - 建议安装 LTS 版本。 - - 安装完成后重新打开 PowerShell,再执行 `node -v` 验证。 - -#### 重装Node.js到22以上的方法 -##### **第一步:安装 nvm-windows(仅首次需要)** -1. **卸载现有 Node.js**:为避免冲突,先去“控制面板” -> “程序和功能”中卸载你当前的 Node.js v18.14.2。 -2. **下载安装**: - - 访问 [nvm-windows 的 GitHub 发布页](https://github.com/coreybutler/nvm-windows/releases)。 - - 下载 `nvm-setup.exe` 文件。 - - 运行安装程序,一路默认选项即可(它会自动配置环境变量)。 - -##### **第二步:使用 nvm 命令升级(以后的日常操作)** -安装完成后,打开一个新的 **命令提示符 (CMD)** 或 **PowerShell**,执行以下命令: - -**查看可安装的版本**(可选): - -```bash -nvm list available -``` - -这会列出所有可用的版本,你可以选择最新版或有特殊需求的版本。 - -**安装最新版 Node.js**(直接复制下面这一整行): - -```bash -nvm install latest -``` - -如果你想安装最新的长期支持版(LTS,更稳定),可以用:`nvm install lts`。 - -**切换并使用新版本**: - -```bash -nvm use latest -``` - -**设置为默认版本**(可选): - -```bash -nvm alias default latest -``` - -设置后,以后每次打开终端都会自动使用这个最新版本,还需要注意nvm安装的node.js跟以前默认安装的node。js路径不一样,在进行CCG配置时需要修改。 - -### 1.2 安装 Claude Code -1. 打开 PowerShell。 -2. 执行安装命令: - -```powershell -npm install -g @anthropic-ai/claude-code -``` - -3. 安装完成后执行: - -```powershell -claude --version -``` - -4. 如果能看到 Claude Code 版本号,说明安装成功。 -5. 如果报错找不到命令,大概率是未添加环境变量,需要找到claude.cmd的安装位置并添加到环境变量。 - - -![](https://cdn.nlark.com/yuque/0/2026/png/29548917/1778298753088-3fd7dbdf-6db9-45ce-960f-a9c91b56217e.png) - -### 1.3 Claude Code的首次配置 -1. 修改_**C:\Users\admin****.****claude\settings.json**_(没有文件可手动新建) - -```json -{ - "env": { - "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1 - } -} -``` - -2. **C:\Users\admin**目录下点击打开隐藏项目,并修改**.claude.json** (没有文件可手动新建),目的是绕过claude的登录检查 - -```json -{ - "hasCompletedOnboarding": true -} -``` - -3. 修改完成后在任何目录使用powershell执行命令:claude,能正常进入界面就是成功了。 - -## 2. 安装 CC Switch -CC Switch 用于管理 Claude Code 的不同模型服务配置,本文使用它配置 DeepSeek。 - -### 2.1 下载 Windows 安装包 -1. 打开浏览器访问: - -```latex -https://github.com/farion1231/cc-switch/releases/tag/v3.14.1 -``` - -2. 页面打开后,拉到最下面的 `Assets` 区域。 -3. 也可以在页面中直接搜索: - -```latex -CC-Switch-v3.14.1-Windows.msi -``` - -4. 点击 `CC-Switch-v3.14.1-Windows.msi` 下载。 - -### 2.2 安装 CC Switch -1. 双击下载的 `CC-Switch-v3.14.1-Windows.msi`。 -2. 按安装向导点击下一步。 -3. 安装路径如无特殊要求,保持默认即可。 -4. 安装完成后,在开始菜单或桌面快捷方式中打开 CC Switch。 - -### 2.3 验证 CC Switch 可用 -1. 打开 CC Switch。 -2. 确认能看到配置列表或新增配置入口。 -3. 如果打不开,记录报错截图:`【待补充:本机报错信息】`。 - -## 3. 在 PyCharm 中安装 CC GUI 插件 -CC GUI 插件用于在 PyCharm 中调用 Claude Code / CC Switch 配置,方便直接在项目内编写接口自动化用例。 - -### 3.1 打开 PyCharm 插件市场 -1. 打开 PyCharm。 -2. 打开接口自动化项目。 -3. 进入菜单: - - `File` → `Settings` - - 或使用快捷键 `Ctrl + Alt + S` -4. 在左侧选择: - - `Plugins` -5. 切换到: - - `Marketplace` - -### 3.2 搜索并安装 CC GUI -1. 在插件搜索框输入: - - -![](https://cdn.nlark.com/yuque/0/2026/png/29548917/1778230873830-2d18afe1-85c8-4b1a-a16c-210e847eaf94.png) - -2. 找到对应插件后点击 `Install`。 -3. 安装完成后,按 PyCharm 提示重启 IDE。 -4. 重启后检查 PyCharm 侧边栏、底部工具窗口或菜单中是否出现 CC GUI 入口。 -5. 检查node.js是否配置,并且版本大于等于22 - - -![](https://cdn.nlark.com/yuque/0/2026/png/29548917/1778298831446-51101824-e214-4046-a5a7-896b05615088.png) - -6. 检查SDK是否安装,且版本建议更新到最新版本 - - -![](https://cdn.nlark.com/yuque/0/2026/png/29548917/1778298967974-23739758-3349-4716-a45b-1f46b2ed4e07.png) - -## 4. 注册并配置 DeepSeek 开发者账号 -### 4.1 注册 DeepSeek 开发者账号 -1. 打开浏览器访问: - -```latex -https://platform.deepseek.com/ -``` - -2. 点击注册或登录。 -3. 按页面提示完成手机号、邮箱或第三方账号登录。 -4. 登录后进入 DeepSeek 开发者控制台。 - -### 4.2 创建 API Key -1. 在 DeepSeek 开发者控制台中找到 API Key 管理入口。 -2. 创建新的 API Key。 -3. 创建后立即复制并保存 API Key。 -4. 请勿把 API Key 提交到 Git、发到群聊或写进测试代码。 - -## 5. 将 DeepSeek 配置到 CC Switch -### 5.1 DeepSeek 配置 -+ 配置信息参考[https://api-docs.deepseek.com/zh-cn/quick_start/agent_integrations/claude_code](https://api-docs.deepseek.com/zh-cn/quick_start/agent_integrations/claude_code) -+ 配置截图参考: - - -![](https://cdn.nlark.com/yuque/0/2026/png/29548917/1778231260675-c66536e0-a3d8-4fc4-a027-7499bf1f3e7e.png) - -### 5.2 验证 Claude Code 使用 DeepSeek -1. 打开 PowerShell。 -2. 进入自己的接口自动化项目目录: - -```powershell -cd D:\workSpace_001\test-automation -``` - -3. 启动 Claude Code: - -```powershell -claude -``` - -4. 输入简单问题验证,例如: - -```latex -你好,你是什么模型。 -``` - -5. 如果能正常回复,说明 Claude Code + CC Switch + DeepSeek 链路可用。 -6. 执行初始化命令生成CLAUDE.md文件 - -```latex -/init -``` - -### 5.3 常见问题 -**问题:401 / Unauthorized** -处理:检查 API Key 是否复制完整,是否有多余空格,DeepSeek 账号是否可用。 - -**问题:模型不存在** -处理:检查模型名是否填写为 `deepseek-chat` 或团队指定模型名。 - -# 三、接口自动化用例编写 -## 1. 准备 `api-test-E10` Skill -### 1.1 手动复制Skill 文件到claude目录下 -```powershell -dir C:\Users\admin\.claude\skills\api-test-E10 -``` - -### 1.2 确认接口自动化项目结构 -1. 用 PyCharm 打开接口自动化项目。 -2. 确认项目中存在接口自动化目录,例如: - -```latex -E10自动化\接口自动化测试 -``` - -3. 确认 `config.py` 中当前启用的 `RunConfig.baseurl` 与浏览器访问的被测环境域名一致。 - -### 1.3 在 Claude Code / CC GUI 中触发 Skill -在 CC GUI 对话中说明要使用 `api-test-E10` Skill,例如: - -```latex -请使用 api-test-E10 Skill 帮我编写接口自动化用例。 -``` - -如果你的环境需要显式引用 Skill 路径,可补充: - -```latex -Skill 路径:C:\Users\admin\.claude\skills\api-test-E10 -``` - -## 2. 使用 CC GUI 编写接口自动化用例 -### 2.1 打开 CC GUI -1. 打开 PyCharm。 -2. 打开接口自动化项目。 -3. 打开 CC GUI 工具窗口 -4. CC GUI设置-供应商管理-选择**使用本地配置信息** -5. CC GUI设置-SKD依赖,确认**Claude Code SDK可用** - -### 2.2 按任务类型提供前置信息 -正式新增或维护接口方法、接口用例前,先确认本次任务类型。新增任务必须提供 5 项;维护任务只强制提供 2 项。 - -#### 新增任务:必须提供 5 项 - -```markdown -# 本次任务信息 -- `[接口方法文件]` = `填写接口方法所在文件路径`(无新增时填:当前用例无新增接口) -- `[接口方法位置]` = `填写接口方法新增位置,例如:文件末尾 / 第123行后 / 某方法后`(无新增时填:当前用例无新增接口) -- `[接口用例文件]` = `填写接口用例所在文件路径` -- `[接口用例位置]` = `填写接口用例新增位置,例如:文件末尾 / 第456行插入 / 某用例后 / 完善某用例` -- `[fixture]` = `选填:接口用例的前后置fixture` -- `[用例名]` = `填写本次新增用例的完整中文功能名称` -``` - -#### 维护任务:只强制提供 2 项 - -```markdown -# 本次维护任务信息 -- `[接口用例文件]` = `填写接口用例所在文件路径` -- `[接口用例位置]` = `填写具体的待维护的单个/多个用例,例如:test_xxx / 某测试类下的多个用例 / 第456行附近的 xxx 用例` -``` - -### 2.3 重要填写规则 -+ `[接口方法文件]` 与 `[接口方法位置]` 必须同时填写真实内容,或同时填写 `当前用例无新增接口`。 -+ 不能保留 `填写接口方法所在文件路径` 这类占位符原文。 -+ `[fixture]` 为选填项,可省略或留空,不参与缺项判定。 -+ 新增任务的 `[用例名]` 要写完整中文功能名,不要只写英文缩写或简单编号。 -+ 维护任务不强制要求 `[接口方法文件]` / `[接口方法位置]` / `[用例名]`,除非定位后确认必须修改接口方法且无法从现有用例反查。 -+ 如果是纯查询、检查环境、启动抓包,不需要填写任务信息。 -+ 一旦进入“新增 / 维护接口方法或用例”阶段,必须按任务类型补齐对应信息。 - -## 3. 用例编写/维护方式总览 -`api-test-E10` Skill 支持新增三种方式、维护四种方式。 - -### 新增任务三种方式 - -| 方式 | 名称 | 适合场景 | 需要你提供 | -| --- | --- | --- | --- | -| 方式 1 | 抓包驱动 | 新接口多、业务链路复杂、希望从真实 UI 操作分析并设计用例 | 抓包环境、UI 操作、勾选接口 | -| 方式 2 | 参考已有用例 | 已有相似用例,只需仿写、改参数、改断言 | 参考用例路径或函数名、差异点 | -| 方式 3 | cURL 手工 | 抓包不可用、接口数量少、能手动复制请求响应 | cURL、响应体、业务说明 | - - -如果你没有指定新增方式,Skill 会让你回复 `1`、`2` 或 `3` 选择。 - -### 维护任务四种方式 - -| 方式 | 名称 | 适合场景 | 需要你提供 | -| --- | --- | --- | --- | -| 方式 1 | 抓包驱动 | 业务链路变化较大、多接口联动、需要同步最新请求路径 | 最新抓包或 UI 操作 | -| 方式 2 | 参考已有用例 | 同类用例结构稳定,只改参数、fixture、断言或调用方式 | 参考用例或同类样本 | -| 方式 3 | cURL 手工 | 少量接口变更明确,不需要完整抓包回溯 | cURL、响应体、差异说明 | -| 方式 4 | pytest 报错驱动 | 只指定待维护用例,让 AI 直接跑 pytest,按最后一个中断报错分类;用例待维护时优先 `/test-fixing`,必要时 `/Debugging` 断点定位 | `[接口用例文件]` 和 `[接口用例位置]` | - -如果你没有指定维护方式,Skill 会让你回复 `1`、`2`、`3` 或 `4` 选择。 - -## 4. 方式 1:抓包驱动编写用例 -抓包服务的完整教程请以本仓库文件为准:[`capture/README.md`](./capture/README.md)。该文件包含 Python、mitmproxy、证书安装、浏览器代理、抓包验证、停止抓包等完整步骤。 - -### 4.1 方式 1 适用场景 -+ 需要覆盖一条完整业务链路。 -+ 页面操作会触发多个接口。 -+ 不确定哪些接口需要新增方法。 -+ 希望 AI 从真实请求中识别新接口、已实现接口和特殊接口。 - -### 4.2 首次使用前配置抓包环境 -按 `capture/README.md` 完成以下事项: - -1. 确认 Python 版本: - -```powershell -python --version -``` - -2. 安装 mitmproxy: - -```powershell -pip install mitmproxy -``` - -3. 验证 mitmproxy: - -```powershell -mitmdump --version -``` - -4. 启动抓包: - -```powershell -cd C:\Users\admin\.claude\skills\api-test-E10\capture -mitmdump -s capture_addon.py --listen-port 12138 -``` - -5. 配置浏览器代理: - - 地址:`127.0.0.1` - - 端口:`12138` -6. 访问 `http://mitm.it` 下载并安装 Windows 证书。 -7. 证书必须安装到: - - `本地计算机` - - `受信任的根证书颁发机构` -8. 打开被测系统完成一次操作,确认生成抓包数据。 - -### 4.3 方式 1 提示词填写步骤 -#### 步骤 1:先发送任务信息 -在 CC GUI 中发送: - -```markdown -请使用 api-test-E10 Skill,按方式1:抓包驱动,帮我编写接口自动化用例。 - -# 本次任务信息 -- `[接口方法文件]` = `E10自动化/接口自动化测试/page_api/【待补充:接口方法文件】.py` -- `[接口方法位置]` = `文件末尾` -- `[接口用例文件]` = `E10自动化/接口自动化测试/test_case/【待补充:接口用例文件】.py` -- `[接口用例位置]` = `文件末尾` -- `[fixture]` = `【选填:接口用例前后置fixture】` -- `[用例名]` = `【待补充:完整中文用例名】` -``` - -#### 步骤 2:让 AI 检查或启动抓包 -继续发送: - -```latex -请检查 api-test-E10 抓包服务是否已启动;如果未启动,请帮我启动抓包服务。 -``` - -#### 步骤 3:你在浏览器中完成业务操作 -1. 确认浏览器代理已切到 `127.0.0.1:12138`。 -2. 打开被测系统。 -3. 按用例需要完成一遍 UI 操作。 -4. 操作完成后回到 CC GUI。 -5. 发送: - -```latex -我已经操作完成,请继续读取抓包结果并生成勾选草稿。 -``` - -#### 步骤 4:勾选需要生成用例的接口 -AI 会生成勾选草稿,通常位置类似: - -```latex -api_test_dwp_temp/capture_selection.md -``` - -你需要: - -1. 打开勾选草稿。 -2. 保留需要写入用例的接口为 `[x]`。 -3. 不需要的接口改成 `[ ]`。 -4. 保存文件。 -5. 回到 CC GUI 发送: - -```latex -我已经完成接口勾选,请按勾选结果分析抓包数据、设计用例、检查相似用例,再生成接口方法和 pytest 用例,并执行最小范围验证。 -``` - -#### 步骤 5:根据 AI 反馈补充信息 -如果 AI 提示以下内容,请按提示补充: - -+ 某个接口是否需要复用已有方法。 -+ 某个字段断言规则不明确。 -+ 某个接口响应体过大或为二进制。 -+ 登录态、租户、组织、用户、流程数据需要你确认。 - -### 4.4 方式 1 完整提示词模板 -```markdown -请使用 api-test-E10 Skill,按方式1:抓包驱动,帮我编写接口自动化用例。 - -# 本次任务信息 -- `[接口方法文件]` = `E10自动化/接口自动化测试/page_api/【待补充】.py` -- `[接口方法位置]` = `文件末尾` -- `[接口用例文件]` = `E10自动化/接口自动化测试/test_case/【待补充】.py` -- `[接口用例位置]` = `文件末尾` -- `[fixture]` = `【选填:接口用例前后置fixture】` -- `[用例名]` = `【待补充:完整中文功能名称】` - -请先检查抓包服务状态;如果未启动,请启动抓包服务。等我在浏览器完成业务操作并回复“继续”后,再读取抓包结果、生成勾选草稿,并按我勾选的接口分析抓包数据、设计用例、检查相似用例,再生成接口方法和 pytest 用例。 -``` - -## 5. 方式 2:参考已有用例编写 -### 5.1 方式 2 适用场景 -+ 已经有相似业务用例。 -+ 新用例只是改页面、改参数、改断言。 -+ 不需要新增接口方法。 -+ 同一类用例需要批量铺开。 - -### 5.2 方式 2 提示词填写步骤 -#### 步骤 1:找到参考用例 -在 PyCharm 中找到你想参考的已有用例,记录: - -+ 参考用例文件路径。 -+ 参考用例函数名。 -+ 新旧用例差异点。 - -示例: - -```latex -参考用例文件:E10自动化/接口自动化测试/test_case/【待补充】.py -参考用例函数:test_【待补充】 -差异点:把【待补充旧功能】改为【待补充新功能】,断言【待补充】字段。 -``` - -#### 步骤 2:判断是否新增接口方法 -如果完全复用已有接口方法: - -```markdown -- `[接口方法文件]` = `当前用例无新增接口` -- `[接口方法位置]` = `当前用例无新增接口` -``` - -如果需要新增接口方法,则填写真实文件和位置。 - -#### 步骤 3:发送方式 2 提示词 -```markdown -请使用 api-test-E10 Skill,按方式2:参考已有用例,帮我编写接口自动化用例。 - -# 本次任务信息 -- `[接口方法文件]` = `当前用例无新增接口` -- `[接口方法位置]` = `当前用例无新增接口` -- `[接口用例文件]` = `E10自动化/接口自动化测试/test_case/【待补充:目标用例文件】.py` -- `[接口用例位置]` = `【待补充:文件末尾 / 某用例后 / 第几行后】` -- `[fixture]` = `【选填:接口用例前后置fixture】` -- `[用例名]` = `【待补充:完整中文用例名】` - -参考用例文件:`E10自动化/接口自动化测试/test_case/【待补充:参考用例文件】.py` -参考用例函数:`test_【待补充:参考函数名】` - -新用例与参考用例的差异: -1. 【待补充:差异点1】 -2. 【待补充:差异点2】 -3. 【待补充:断言要求】 - -请先阅读参考用例和相关接口方法,按现有编码风格仿写,不要改动无关代码。完成后执行最小范围 pytest 验证。 -``` - -#### 步骤 4:按 AI 提问补充业务差异 -AI 可能会要求补充: - -+ 新用例使用的数据来源。 -+ 是否需要前置创建数据。 -+ 是否需要清理数据。 -+ 断言字段和预期值。 -+ 是否复用参考用例中的 fixture。 - -请根据实际业务回复,不确定的内容可以明确写: - -```latex -该字段我不确定,请先按参考用例保持一致,无法判断的位置留 TODO 或向我确认后再写。 -``` - -### 5.3 方式 2 完整提示词模板 -```markdown -请使用 api-test-E10 Skill,按方式2:参考已有用例,帮我编写接口自动化用例。 - -# 本次任务信息 -- `[接口方法文件]` = `当前用例无新增接口` -- `[接口方法位置]` = `当前用例无新增接口` -- `[接口用例文件]` = `E10自动化/接口自动化测试/test_case/【待补充】.py` -- `[接口用例位置]` = `文件末尾` -- `[fixture]` = `【选填:接口用例前后置fixture】` -- `[用例名]` = `【待补充:完整中文功能名称】` - -参考用例文件:`E10自动化/接口自动化测试/test_case/【待补充】.py` -参考用例函数:`test_【待补充】` - -请仿照参考用例新增一个用例,差异如下: -1. 【待补充】 -2. 【待补充】 -3. 【待补充】 - -要求: -- 保持现有接口方法调用风格。 -- 不修改无关代码。 -- 如果发现已有接口方法可复用,优先复用。 -- 完成后运行当前用例文件中新增用例的最小范围 pytest。 -``` - -## 6. 方式 3:cURL 手工编写 -### 6.1 方式 3 适用场景 -+ 抓包服务暂时不可用。 -+ 抓包数据太多,不方便筛选。 -+ 接口数量较少,可以手动复制 cURL。 -+ 你已经从浏览器 DevTools、Apifox、Postman 或其他工具拿到了请求和响应。 - -### 6.2 获取 cURL 和响应体 -#### 从 Chrome DevTools 获取 cURL -1. 打开 Chrome。 -2. 按 `F12` 打开开发者工具。 -3. 切换到 `Network` 面板。 -4. 勾选 `Preserve log`,避免页面跳转后请求丢失。 -5. 在页面上完成业务操作。 -6. 找到目标接口请求。 -7. 右键请求。 -8. 选择: - - `Copy` - - `Copy as cURL` 或 `Copy as cURL (bash)` -9. 打开请求的 `Response` 或 `Preview`,复制接口响应体。 - -### 6.3 方式 3 提示词填写步骤 -#### 步骤 1:整理接口信息 -每个接口建议按以下格式整理: - -```latex -## 接口 1:【待补充:接口用途】 - -### cURL -【待补充:粘贴 cURL】 - -### 响应体 -【待补充:粘贴响应体】 -``` - -如果有多个接口,按 `接口 1`、`接口 2`、`接口 3` 依次排列。 - -#### 步骤 2:发送方式 3 提示词 -```markdown -请使用 api-test-E10 Skill,按方式3:cURL 手工,帮我编写接口自动化用例。 - -# 本次任务信息 -- `[接口方法文件]` = `E10自动化/接口自动化测试/page_api/【待补充:接口方法文件】.py` -- `[接口方法位置]` = `文件末尾` -- `[接口用例文件]` = `E10自动化/接口自动化测试/test_case/【待补充:接口用例文件】.py` -- `[接口用例位置]` = `文件末尾` -- `[fixture]` = `【选填:接口用例前后置fixture】` -- `[用例名]` = `【待补充:完整中文用例名】` - -下面是本次用例涉及的接口 cURL 和响应体,请解析请求方法、URL、参数、请求体和响应断言,生成接口方法和 pytest 用例。 - -## 接口 1:【待补充:接口用途】 - -### cURL -【待补充:粘贴 cURL】 - -### 响应体 -【待补充:粘贴响应体】 - -## 接口 2:【可选,待补充】 - -### cURL -【待补充:粘贴 cURL】 - -### 响应体 -【待补充:粘贴响应体】 - -要求: -- 优先检查是否已有相同 URL 的接口方法,已有则复用。 -- 新增接口方法时按项目现有 page_api 风格编写。 -- 用例断言请基于响应体中的稳定字段,不要断言时间戳、随机 ID 等不稳定字段。 -- 不明确的业务字段请先向我确认,不要乱写。 -- 完成后执行最小范围 pytest 验证。 -``` - -#### 步骤 3:补充字段说明 -如果 cURL 或响应中有业务字段不容易判断,请额外补充说明,例如: - -```markdown -字段说明: -- `name`:本次创建的数据名称,需要使用随机后缀避免重复。 -- `status`:预期为启用状态。 -- `id`:由前一个接口返回,后续接口需要复用。 -- `createTime`:动态时间,不需要强断言。 -``` - -### 6.4 方式 3 完整提示词模板 -```markdown -请使用 api-test-E10 Skill,按方式3:cURL 手工,帮我编写接口自动化用例。 - -# 本次任务信息 -- `[接口方法文件]` = `E10自动化/接口自动化测试/page_api/【待补充】.py` -- `[接口方法位置]` = `文件末尾` -- `[接口用例文件]` = `E10自动化/接口自动化测试/test_case/【待补充】.py` -- `[接口用例位置]` = `文件末尾` -- `[fixture]` = `【选填:接口用例前后置fixture】` -- `[用例名]` = `【待补充:完整中文功能名称】` - -业务目标: -【待补充:本用例要验证什么】 - -接口链路: -1. 【待补充:第一步接口用途】 -2. 【待补充:第二步接口用途】 -3. 【待补充:最终断言】 - -## 接口 1:【待补充】 - -### cURL -【待补充】 - -### 响应体 -【待补充】 - -字段说明: -- 【待补充】 - -请先按 URL 查找是否已有接口方法;已有则复用,没有再新增。请按现有项目风格生成接口方法和 pytest 用例,不要改动无关代码。遇到不明确字段请先问我。 -``` - -## 7. 编写完成后的验证步骤 -### 7.1 最小范围运行 pytest -AI 完成代码后,通常会运行最小范围测试。你也可以手动执行: - -```powershell -pytest E10自动化\接口自动化测试\test_case\【待补充:用例文件】.py -k "【待补充:用例函数关键字】" -``` - -### 7.2 如果测试失败 -把失败日志完整发给 CC GUI,并说明: - -```latex -这是刚才新增用例的 pytest 失败日志,请使用 api-test-E10 Skill 按真实报错修复,只修改本次相关代码,不要改动无关用例。 -``` - -### 7.3 验证通过后检查改动 -1. 在 PyCharm 中查看 Git 变更。 -2. 确认只修改了本次相关文件。 -3. 确认没有提交运行期产物,例如: - - `capture/latest.jsonl` - - `api_test_dwp_temp/` - - `capture_selection.md` -4. 确认新增用例名称、断言和测试数据符合预期。 - -## 8. 推荐使用流程 -首次配置时按以下顺序执行: - -1. 安装 Node.js。 -2. 安装 Claude Code。 -3. 安装 CC Switch。 -4. 在 DeepSeek 平台注册账号并创建 API Key。 -5. 在 CC Switch 中新增 DeepSeek 配置。 -6. 在 PyCharm 中安装 CC GUI 插件。 -7. 打开接口自动化项目。 -8. 在 CC GUI 中验证 AI 可正常回复。 -9. 确认 `api-test-E10` Skill 可触发。 -10. 首次使用方式 1 时,按 `capture/README.md` 配置 mitmproxy、证书和浏览器代理。 -11. 按本教程“三、接口自动化用例编写”的第 4、5、6 节选择一种方式编写用例。 -12. 执行最小范围 pytest 验证。 - -日常编写用例时按以下顺序执行: - -1. 打开 PyCharm 和 CC GUI。 -2. 先确认本次是新增任务还是维护任务。 -3. 新增任务准备 5 项任务信息;维护任务准备 2 项维护任务信息。 -4. 新增任务选择方式 1、方式 2 或方式 3;维护任务选择方式 1、方式 2、方式 3 或方式 4。 -5. 按对应模板发送提示词。 -6. 根据 AI 提问补充业务细节。 -7. 查看生成代码。 -8. 运行或确认 AI 已运行最小范围 pytest。 -9. 处理失败日志直到通过。 -10. 检查 Git 变更,避免提交运行期产物。 - -## 9. 常见问题 -### 9.1 Claude Code 能打开,但 CC GUI 不可用 -处理步骤: - -1. 确认 PyCharm 已安装 CC GUI 插件。 -2. 重启 PyCharm。 -3. 检查插件入口:`View` → `Tool Windows`。 -4. 确认 CC Switch 已切换到 DeepSeek 配置。 -5. 仍失败时记录错误:`【待补充:错误截图或日志】`。 - -### 9.2 AI 没有按 `api-test-E10` 规范执行 -处理:在提示词开头明确写: - -```latex -请使用 api-test-E10 Skill,并严格按 SKILL.md 的新增/维护前置门禁和对应方式执行。 -``` - -### 9.3 提示缺少任务信息 -处理:先确认任务类型,再检查对应信息。 - -新增任务检查是否完整填写了 5 项: - -+ `[接口方法文件]` -+ `[接口方法位置]` -+ `[接口用例文件]` -+ `[接口用例位置]` -+ `[fixture]`(选填,可省略或留空) -+ `[用例名]` - -如果新增任务不新增接口方法,前两项必须同时填写 `当前用例无新增接口`。 - -维护任务检查是否完整填写了 2 项: - -+ `[接口用例文件]` -+ `[接口用例位置]` - -### 9.4 抓包服务启动失败 -处理:按 `capture/README.md` 排查,重点检查: - -+ Python 是否安装。 -+ `mitmdump --version` 是否可用。 -+ `12138` 端口是否被占用。 -+ 证书是否安装到 `本地计算机` 的 `受信任的根证书颁发机构`。 -+ 浏览器代理是否为 `127.0.0.1:12138`。 - -### 9.5 不知道选哪种编写方式 -建议: - -+ 有真实页面操作、新链路复杂:优先选方式 1。 -+ 有非常相似的已有用例:优先选方式 2。 -+ 只有接口请求和响应,没有抓包环境:选方式 3。 -+ 维护已有用例且希望 AI 自行运行 pytest 看报错修复:选维护方式 4;默认优先 `/test-fixing`,只有维护困难或前后接口/调用栈信息不明确时才使用 `/Debugging`。 - -## 10. 附录:可直接复制的启动检查提示词 -```markdown -请使用 api-test-E10 Skill,帮我检查当前接口自动化环境是否可用。 - -请检查: -1. 当前项目结构是否符合接口自动化项目要求。 -2. 是否能找到 `E10自动化/接口自动化测试/config.py`。 -3. 当前 `RunConfig.baseurl` 是否可读取。 -4. `api-test-E10` Skill 相关工具是否存在。 -5. 如果我要使用方式1,请检查抓包服务是否运行。 - -这次只是环境检查,不新增或修改用例。 -``` - diff --git a/doc/mode_java_controller_source.md b/doc/mode_java_controller_source.md index 5571d7d..42da883 100644 --- a/doc/mode_java_controller_source.md +++ b/doc/mode_java_controller_source.md @@ -67,7 +67,8 @@ Jacoco 覆盖染色只作为参考;接口是否已被接口自动化覆盖, - 第一阶段:只做基础状态/成功状态断言,并打印完整接口返回值。 - 执行 pytest 获取真实返回。 - 第二阶段:依据真实返回补充结构和关键字段断言。 - - 再执行 pytest 闭环,直到通过或触发 3 次调试上限。 + - 补充断言后需要删除掉第一阶段新增的打印print() + - 最后执行 pytest 闭环,直到通过或触发 3 次调试上限。 11. **3 次调试上限**: - 基于当前源码信息调试 3 次仍无法通过时,停止继续尝试。 - 向用户总结无法请求通过的原因,必须判断更可能属于请求信息不正确、payload 缺字段、前置变量错误、登录态/权限不足、环境数据缺失、接口本身不可用等哪一类。