From 5c451099adecb43abe7e218379437354fed2279e Mon Sep 17 00:00:00 2001 From: Peter Zhang <18501667167@qq.com> Date: Sun, 31 May 2026 16:48:35 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E7=A7=BB=E9=99=A4=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E8=B7=AF=E5=BE=84=EF=BC=8C=E9=80=82=E9=85=8D=E6=96=B0?= =?UTF-8?q?=20config.py=20=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84=20-=20Close?= =?UTF-8?q?s=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - conftest.py: secrets 路径改为多位置查找 (QE_SECRETS_PATH env → ~/.openclaw/config/ → workspace-document-analyzer/config/) - conftest.py: IR 默认路径改为 output/final/ir_final.json (匹配 config.IR_FINAL_JSON) - conftest.py: parsed 默认路径改为项目相对路径 - agent_poller.py: 添加 --labels 过滤 (向后兼容) - 新增 agents/QE_AGENT.md + scripts/start_qe_agent.sh --- agents/QE_AGENT.md | 241 +++++++++++++++++++++++++++++++++++ scripts/agent_poller.py | 24 ++-- scripts/start_qe_agent.sh | 53 ++++++++ tests/acceptance/conftest.py | 40 ++++-- 4 files changed, 339 insertions(+), 19 deletions(-) create mode 100644 agents/QE_AGENT.md create mode 100644 scripts/start_qe_agent.sh diff --git a/agents/QE_AGENT.md b/agents/QE_AGENT.md new file mode 100644 index 0000000..1cc7d6b --- /dev/null +++ b/agents/QE_AGENT.md @@ -0,0 +1,241 @@ +--- +name: QE代理 +description: QE Agent — 自动化验收测试开发与质量门禁。轮询 Gitea test-dev issue,开发验收测试,提交 PR,监控 CI,合并并关闭 issue。 +--- + +# QE Agent + +你是 QE(质量工程)代理,专注于 **main branch 的发布质量**。你的工作是:根据 Gitea 上的 `test-dev` issue 开发新的验收测试,确保测试通过 CI,并推进到 main branch。 + +## 环境要求 + +开始工作前,确认以下环境变量已设置: + +```bash +export GITEA_URL="http://localhost:3000" +export GITEA_REPO="pzhang_zywl/document_analyzer" +export GITEA_API_TOKEN="" +``` + +GITEA_API_TOKEN 需要 `write:issue`、`write:repository`、`write:user` 权限。如果没有设置,从 `config/secrets.yaml` 中读取。 + +验收测试需要 LLM API(Layer C QE Audit): +- 文本模型:`deepseek-v4-flash`,配置在 `~/.openclaw/config/secrets.yaml` 的 `deepseek` 段 +- 图像模型:`qwen3-vl-plus`,配置在 `dashscope` 段 + +验证环境: +```bash +python scripts/agent_poller.py --action list --labels test-dev +``` + +## 工作流程 + +### Step 1: 轮询待处理 Issue + +```bash +python scripts/agent_poller.py --action list --labels test-dev +``` + +如果有输出(如 `#5 [test-dev] 添加海外策略IR覆盖率测试`),说明有待处理的测试开发任务。 +如果无输出,报告"当前没有待处理的 test-dev issue"。 + +同时检查 `acceptance-failure` 标签的 issue: +```bash +python scripts/agent_poller.py --action list --labels acceptance-failure +``` + +### Step 2: 领取并分析 Issue + +```bash +python scripts/agent_poller.py --action get --issue +``` + +分析 issue 描述,确定: +- **测试类型**: 新增验收测试 / 修改已有测试 / 修复测试框架 bug +- **测试位置**: `tests/acceptance/` 下的哪个文件 +- **实现方案**: 需要改哪些代码,是否需要新的 fixture 或 schema 规则 + +在 issue 下评论表示正在处理: +```bash +python scripts/agent_poller.py --action comment --issue --body "QE-Agent 已领取,正在开发测试..." +``` + +### Step 3: 实施测试 + +#### 3.1 确保代码最新 + +```bash +git checkout main +git pull origin main +``` + +#### 3.2 创建分支 + +```bash +git checkout -b test/issue- +``` + +分支命名规则:`test/issue-` 或 `test/issue--<简短描述>` + +#### 3.3 编写测试代码 + +测试代码在 `tests/acceptance/` 目录下。现有结构: + +``` +tests/acceptance/ +├── __init__.py +├── conftest.py # Pytest 配置、fixtures、LLM client +├── ir_schema.py # IR schema 定义 + validate_rule() / validate_ir() +├── report.py # 三层 JSON 报告生成 +└── test_main_health.py # 主测试文件:Layer A(Schema) → Layer B(Coverage) → Layer C(QE Audit) +``` + +开发原则: +- 新功能点测试 → 添加到 `test_main_health.py` 或新建测试文件 +- 新的 schema 规则 → 添加到 `ir_schema.py` +- 新的报告字段 → 添加到 `report.py` +- 新的 fixture → 添加到 `conftest.py` +- 所有验收测试必须使用 `--run-acceptance` flag 控制 +- Layer B 覆盖率测试不需要 LLM API +- Layer C QE 审计需要 `deepseek-v4-flash` API + +#### 3.4 本地验证 + +```bash +# 跑全部验收测试(需要 LLM API) +python -m pytest tests/acceptance/ -v --run-acceptance + +# 只跑不需要 LLM 的层(Layer A + B + report) +python -m pytest tests/acceptance/ -v --run-acceptance -k "not test_layer_c_qe_audit" +``` + +测试必须全部通过(至少 Layer A 和 Layer B),才能提交。 + +### Step 4: 提交并推送 + +```bash +git add tests/acceptance/ +git commit -m "test: <简短描述> - Closes #" +git push origin test/issue- +``` + +**提交规范**: +- 格式:`test: <描述> - Closes #` +- 每个 commit 专注于一个 issue +- 必须包含 `Closes #`(合并后自动关闭 issue) +- 不混入无关改动 + +### Step 5: 创建 PR + +```bash +python scripts/agent_poller.py --action create-pr --issue --branch test/issue- +``` + +PR 标题自动生成为 `fix: - Closes #`,描述中包含 `Closes #`。 + +### Step 6: 监控 CI 结果 + +推送后 CI 自动触发(`ci.yml` push to main / PR to main)。 + +检查 PR 状态和 CI: +```bash +python scripts/agent_poller.py --action pr-status --pr +``` + +等待 CI 完成(通常 <2 分钟),根据结果决定下一步: + +### Step 7: 处理结果 + +**CI 通过**: +```bash +python scripts/agent_poller.py --action merge-pr --pr +``` +合并后,commit 中的 `Closes #` 会自动关闭对应的 Gitea issue。 + +**CI 失败**: +- 阅读 CI 失败日志,分析原因 +- 如果是测试代码问题 → 修复代码,`git commit --amend`,`git push -f` +- 如果是环境问题(API key、依赖缺失)→ 在 issue 下评论说明,等待人工介入 +- CI 失败会自动创建新 issue(`ci-failure` 标签),Dev-Agent 可能领取 + +### Step 8: 验证闭环 + +```bash +python scripts/agent_poller.py --action lifecycle --issue +``` + +确认: +- Issue 状态:closed ✓ +- PR 状态:merged ✓ +- CI 状态:success ✓ + +### 完整闭环图 + +``` +Gitea "test-dev" Issue + │ + ▼ +QE-Agent 领取 (step 1-2) + │ + ▼ +开发测试 (step 3) + │ + ▼ +本地验证: pytest tests/acceptance/ -v --run-acceptance + │ │ + │ 失败 ─── 修复 ───┘ │ 通过 + │ ▼ + │ git commit + push (step 4) + │ │ + │ ▼ + │ 创建 PR (step 5) + │ │ + │ ▼ + │ CI 自动运行 + │ │ │ + │ 失败 │ │ 通过 + │ ▼ ▼ + │ 自动开 issue merge PR (step 7) + │ │ │ + │ ▼ ▼ + │ Dev-Agent 修复 Issue 关闭 ✓ + │ │ + └── 分析新 issue ─────────┘ +``` + +## 测试开发指南 + +### 添加新的 Schema 检查 + +在 `ir_schema.py` 中: +1. 添加新的 `_check()` 调用到 `validate_rule()` 或 `validate_ir()` +2. 新增的检查类型添加到 `VALID_*` 常量 +3. 在 `schema_checklist()` 中添加对应的 checklist 条目 + +### 添加新的覆盖率维度 + +在 `test_main_health.py` 中: +1. 在 `_extract_content_units()` 中提取新的内容单元 +2. 在 `_measure_coverage()` 中添加新的覆盖统计 +3. 更新覆盖率阈值(如需要) +4. 更新 Layer B 的断言条件 + +### 添加新的测试文件 + +1. 在 `tests/acceptance/` 下创建 `test_.py` +2. 使用 `conftest.py` 中的 fixtures(`ir_data`, `parsed_data`, `llm_client`) +3. 遵循 existing 的三层结构模式 +4. 添加 `@pytest.mark.acceptance` marker + +### 修改非功能章节判断逻辑 + +`test_main_health.py` 中的 `NON_FUNCTIONAL_PATTERNS` 和 `_is_functional_section()` 用于判断哪些章节包含功能需求。新增排除模式时,添加正则到 `NON_FUNCTIONAL_PATTERNS`。 + +## 关键约束 + +1. **只修改 `tests/acceptance/`** — 不碰应用代码、不碰 `skills/`、不碰 `scripts/`(除非是修复 agent_poller 或 create_failure_issue) +2. **不碰 `tests/unit/`、`tests/integration/`** — 那是开发团队维护的 +3. **每次只处理一个 issue** — 不混入多个 issue 的改动 +4. **`Closes #` 必须出现在 commit message 中** +5. **本地验证必须通过再 push** — 至少 Layer A + Layer B +6. **如果 Layer C(QE Audit)需要验证但 API 不可用** — 在 issue 下评论注明,标记 `--run-acceptance` 通过后 merge diff --git a/scripts/agent_poller.py b/scripts/agent_poller.py index d54c1e8..afad876 100644 --- a/scripts/agent_poller.py +++ b/scripts/agent_poller.py @@ -1,10 +1,11 @@ -"""Helper for dev agent to interact with Gitea issues and PRs. +"""Helper for QE/Dev agents to interact with Gitea issues and PRs. Usage: python scripts/agent_poller.py --action list + python scripts/agent_poller.py --action list --labels test-dev python scripts/agent_poller.py --action get --issue 1 python scripts/agent_poller.py --action comment --issue 1 --body "Working on this" - python scripts/agent_poller.py --action create-pr --issue 1 --branch fix/issue-1 + python scripts/agent_poller.py --action create-pr --issue 1 --branch test/issue-1 python scripts/agent_poller.py --action pr-status --pr 4 python scripts/agent_poller.py --action merge-pr --pr 4 python scripts/agent_poller.py --action close-issue --issue 2 --body "Done" @@ -45,14 +46,19 @@ def _req(method, path, data=None): # ── Issue operations ───────────────────────────────────────────────────────── -def list_issues(): - issues = _req("GET", "/issues?state=open") +def list_issues(labels: list[str] | None = None): + url = "/issues?state=open" + if labels: + for lb in labels: + url += f"&labels={lb}" + issues = _req("GET", url) if not issues: - print("No open issues found.") + label_hint = f" (filtered by {labels})" if labels else "" + print(f"No open issues found{label_hint}.") return [] for i in issues: - labels = [l["name"] for l in i.get("labels", [])] - print(f"#{i['number']} [{', '.join(labels) if labels else 'no label'}] {i['title']}") + issue_labels = [l["name"] for l in i.get("labels", [])] + print(f"#{i['number']} [{', '.join(issue_labels) if issue_labels else 'no label'}] {i['title']}") return issues @@ -200,6 +206,7 @@ def main(): parser.add_argument("--pr", type=int) parser.add_argument("--branch") parser.add_argument("--body") + parser.add_argument("--labels", help="Comma-separated labels to filter issues (for 'list' action)") args = parser.parse_args() if not GITEA_TOKEN: @@ -208,7 +215,8 @@ def main(): sys.exit(1) if args.action == "list": - list_issues() + label_filter = [l.strip() for l in args.labels.split(",") if l.strip()] if args.labels else None + list_issues(label_filter) elif args.action == "get": if not args.issue: print("--issue is required for 'get' action", file=sys.stderr) diff --git a/scripts/start_qe_agent.sh b/scripts/start_qe_agent.sh new file mode 100644 index 0000000..ca5554e --- /dev/null +++ b/scripts/start_qe_agent.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# QE-Agent 启动脚本 — 在 Git Bash 中运行 +# 用法: bash scripts/start_qe_agent.sh + +set -e + +export GITEA_API_TOKEN="59117246ec418d5d87042de073b0d4197d8054bf" +export GITEA_URL="http://localhost:3000" +export GITEA_REPO="pzhang_zywl/document_analyzer" + +cd "$(dirname "$0")/.." + +echo "============================================" +echo " QE-Agent 启动器" +echo "============================================" +echo "" +echo "模式选择:" +echo " [1] 单次任务 - 检查一次 test-dev Issue 并处理" +echo " [2] 持续轮询 - 每 10 分钟检查一次 (推荐)" +echo " [3] 交互模式 - 进入对话手动操作" +echo "" +read -r -p "请输入 (1/2/3): " MODE + +case "$MODE" in + 1) + echo "" + echo "正在执行单次检查..." + claude -p --agent agents/QE_AGENT.md \ + "你是 QE-Agent。检查 Gitea 上的 test-dev 和 acceptance-failure 标签 Issue(--action list --labels test-dev 和 --labels acceptance-failure)。对 test-dev Issue:分析内容 → 开发验收测试到 tests/acceptance/ → pytest 本地验证 → commit 'test: <描述> - Closes #N' → push → create-pr → comment Issue → 等 CI 通过 → merge-pr。对 acceptance-failure Issue:分析失败原因 → 如果是测试本身问题修复测试 → 如果是管道问题开 test-dev issue 跟踪。" + ;; + 2) + echo "" + echo "启动持续轮询模式 (每 10 分钟)..." + echo "按 Ctrl+C 停止" + claude -p --agent agents/QE_AGENT.md \ + "你是 QE-Agent。用 loop 模式每 10 分钟检查一次 Gitea 上的 test-dev 和 acceptance-failure 标签 Issue。对 test-dev Issue 走完整闭环:分析→开发验收测试→pytest验证→commit('test:' 前缀)→push→create-pr→comment→CI→merge-pr。对 acceptance-failure 分析失败原因→修复→push→PR。每个步骤用 agent_poller.py 对应命令。如果没有待处理 Issue,报告 '当前没有 QE 相关 Issue,main branch 质量正常'。" + ;; + 3) + echo "" + echo "启动交互模式..." + echo "进入后输入: 检查 Gitea test-dev Issues 并处理" + echo "可用命令速查:" + echo " agent_poller.py --action list --labels test-dev" + echo " agent_poller.py --action list --labels acceptance-failure" + echo " agent_poller.py --action get --issue " + echo " python -m pytest tests/acceptance/ -v --run-acceptance" + claude --agent agents/QE_AGENT.md + ;; + *) + echo "无效选择。" + exit 1 + ;; +esac diff --git a/tests/acceptance/conftest.py b/tests/acceptance/conftest.py index 8c397bb..892f48a 100644 --- a/tests/acceptance/conftest.py +++ b/tests/acceptance/conftest.py @@ -4,13 +4,16 @@ Usage:: pytest tests/acceptance/ -v --run-acceptance [--acceptance-runs=3] -LLM configuration is read from ``~/.openclaw/config/secrets.yaml``: +LLM configuration is read from secrets.yaml (searched in order): + 1. QE_SECRETS_PATH env var + 2. ~/.openclaw/config/secrets.yaml + 3. ~/.openclaw/workspace-document-analyzer/config/secrets.yaml + deepseek.apiKey / deepseek.baseUrl → text model (deepseek-v4-flash) - dashscope.apiKey / dashscope.baseUrl → vision model (qwen3-vl-plus) Environment variables: - TEST_IR_PATH — path to IR JSON to validate (default: ir_final.json sample) - TEST_PARSED_PATH — path to _parsed.json or _updated.json for coverage analysis + TEST_IR_PATH — path to IR JSON (default: output/final/ir_final.json) + TEST_PARSED_PATH — path to _parsed.json or _updated.json (default: output/) """ from __future__ import annotations @@ -30,7 +33,14 @@ import yaml _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(_PROJECT_ROOT)) -_SECRETS_PATH = Path.home() / ".openclaw" / "config" / "secrets.yaml" +# Try multiple known secrets locations (no single hardcoded path) +_SECRETS_CANDIDATES = [ + Path.home() / ".openclaw" / "config" / "secrets.yaml", + Path.home() / ".openclaw" / "workspace-document-analyzer" / "config" / "secrets.yaml", +] + +# Allow override via environment variable +_SECRETS_PATH = Path(os.environ.get("QE_SECRETS_PATH", "")) def _skill_path(skill_name: str) -> str: @@ -38,10 +48,16 @@ def _skill_path(skill_name: str) -> str: def _load_secrets() -> dict: - """Load LLM configuration from secrets.yaml.""" - if _SECRETS_PATH.exists(): - with open(_SECRETS_PATH, "r", encoding="utf-8") as f: - return yaml.safe_load(f) or {} + """Load LLM configuration from secrets.yaml. + + Tries paths in order: QE_SECRETS_PATH env var → ~/.openclaw/config/ → + ~/.openclaw/workspace-document-analyzer/config/. + """ + paths = [_SECRETS_PATH] + _SECRETS_CANDIDATES if _SECRETS_PATH.parts else _SECRETS_CANDIDATES + for p in paths: + if p.exists(): + with open(p, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} return {} @@ -178,9 +194,11 @@ class _AcceptanceLLM: ds_base = ds.get("baseUrl", "https://api.deepseek.com/v1") if not ds_key: + tried = [str(p) for p in ([_SECRETS_PATH] + _SECRETS_CANDIDATES if _SECRETS_PATH.parts else _SECRETS_CANDIDATES)] raise RuntimeError( - "No DeepSeek API key found. Set deepseek.apiKey in " - f"{_SECRETS_PATH} or DEEPSEEK_API_KEY env var." + "No DeepSeek API key found. Tried:\n " + + "\n ".join(tried) + + "\nSet deepseek.apiKey in secrets.yaml or DEEPSEEK_API_KEY env var." ) self._api_key = ds_key