Compare commits

..

13 Commits

Author SHA1 Message Date
pzhang_zywl 2ed36c0013 test: 实现端到端验收测试流程 (run_pipeline.py + acceptance.yml) - Closes #12
CI / test (pull_request) Successful in 8s
- scripts/run_pipeline.py: 完整管道运行器 (docx → IR → acceptance tests)
- acceptance.yml: 更新为 workflow_dispatch,支持 --input/--parsed/--test 三种模式
- 失败时自动创建 acceptance-failure issue
2026-05-31 17:01:30 +08:00
pzhang_zywl cd721634dd Merge pull request 'fix: [test-dev] 根据最新的document_analyzer源代码更新测试代码 - Closes #10' (#11) from test/issue-10 into main
CI / test (push) Successful in 9s
2026-05-31 16:49:51 +08:00
pzhang_zywl 5c451099ad test: 移除硬编码路径,适配新 config.py 目录结构 - Closes #10
CI / test (pull_request) Successful in 7s
- 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
2026-05-31 16:48:35 +08:00
pzhang_zywl 2e36710813 Merge pull request 'fix: 改进输入文件处理 - Closes #8' (#9) from dev/issue-8-improve-input-handling into main
CI / test (push) Successful in 7s
2026-05-31 16:17:59 +08:00
pzhang_zywl c2affcad42 fix: 移除 hardcode 输入文件路径,完善输入验证 - Closes #8
CI / test (pull_request) Successful in 9s
- 移除 _DEFAULT_INPUT 硬编码默认输入文件路径
- INPUT_JSON 仅从 IR_INPUT_JSON 环境变量获取
- load_input_document() 无输入时给出明确错误提示
- 新增 test_no_hardcoded_input_file / test_set_input_file_accepts_none

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:16:49 +08:00
pzhang_zywl 73291803b6 Merge pull request 'docs: DEV_AGENT.md 完善自举能力' (#7) from dev/issue-3-unified-output-dir into main
CI / test (push) Successful in 9s
2026-05-31 15:51:08 +08:00
pzhang_zywl e792fac6c0 docs: DEV_AGENT.md 完善自举能力,新增命令速查表和闭环检查清单
CI / test (pull_request) Successful in 9s
- 新增 agent_poller 8 命令速查表
- 新增闭环完成 12 步检查清单
- 步骤 4 增加 PR 创建后评论 Issue 的步骤
- start_dev_agent.sh 启动提示更新为完整闭环指令

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:49:42 +08:00
pzhang_zywl e71ab9715e Merge pull request 'fix: 优化输出文件目录 - Closes #3' (#6) from dev/issue-3-unified-output-dir into main
CI / test (push) Successful in 7s
2026-05-31 14:42:42 +08:00
pzhang_zywl c31ddd0bb3 fix: agent_poller merge-pr 处理 Gitea 空响应体 - Closes #3
CI / test (pull_request) Successful in 7s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 14:41:23 +08:00
pzhang_zywl 82829bf216 Merge pull request 'fix: 优化输出文件目录 - Closes #3' (#5) from dev/issue-3-unified-output-dir into main
CI / test (push) Successful in 6s
2026-05-31 14:40:03 +08:00
pzhang_zywl 884848f15f fix: 统一输出文件目录结构 - Closes #3
CI / test (pull_request) Successful in 7s
- 新增 PROJECT_OUTPUT (项目根/output/),统一所有输出文件
- IR 中间产物 → output/ir/,最终交付物 → output/final/
- agent_poller.py 新增 pr-status/merge-pr/close-issue/lifecycle 命令
- DEV_AGENT.md 同步更新完整闭环流程
- 更新 conftest/test_sample 中的默认路径

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 14:38:37 +08:00
pzhang_zywl 9be264250a Merge pull request 'Fix #2: 完善现有代码的UT' (#4) from dev/issue-2-improve-ut-coverage into main
CI / test (push) Successful in 7s
2026-05-31 14:28:50 +08:00
pzhang_zywl 682dedb4b4 fix: 完善 UT 覆盖,统一 pytest 测试发现 - Closes #2
CI / test (pull_request) Successful in 9s
- 新建 pytest.ini 统一 test discovery(tests/ + skills/ir_generation_skill/tests/)
- test_step1~3 转换为 pytest 兼容格式,无输出文件时自动 skip
- 新增 tests/test_detect_conflicts.py(18 个纯函数单测)
- 新增 tests/test_config.py(7 个配置模块单测)
- CI 改为 pytest -v 使用 pytest.ini testpaths
- DEV_AGENT.md 新增 PR 提交规范

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 00:07:07 +08:00
19 changed files with 1466 additions and 114 deletions
+34 -26
View File
@@ -3,23 +3,23 @@ name: QE Acceptance Tests
on:
workflow_dispatch:
inputs:
prd_path:
description: 'Path to .docx PRD file (absolute)'
required: false
default: ''
parsed_path:
description: 'Path to pre-parsed _updated.json (skip doc_parser if set)'
required: false
default: ''
acceptance_runs:
description: 'Layer B stability runs (1 = skip stability testing)'
description: 'Layer B stability runs (1 = skip)'
required: false
default: '1'
ir_path:
description: 'Path to IR JSON file (relative to workspace)'
required: false
default: 'output/ir_final.json'
parsed_path:
description: 'Path to _parsed.json or _updated.json (relative to workspace)'
required: false
default: 'output/车机娱乐系统禁止功能文档_精简_updated.json'
jobs:
acceptance:
runs-on: shell
timeout-minutes: 30
timeout-minutes: 60
steps:
- name: Checkout main branch
run: |
@@ -29,26 +29,34 @@ jobs:
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run QE Acceptance Tests
run: >-
python -m pytest tests/acceptance/ -v
--run-acceptance
--acceptance-runs=${{ github.event.inputs.acceptance_runs }}
--ir-path=${{ github.event.inputs.ir_path }}
--parsed-path=${{ github.event.inputs.parsed_path }}
--tb=long
- name: Run pipeline + acceptance tests
run: |
if [ -n "${{ github.event.inputs.prd_path }}" ]; then
python scripts/run_pipeline.py --input "${{ github.event.inputs.prd_path }}" --test
elif [ -n "${{ github.event.inputs.parsed_path }}" ]; then
python scripts/run_pipeline.py --parsed "${{ github.event.inputs.parsed_path }}" --test
else
# No input provided — run acceptance on existing output if present
python -m pytest tests/acceptance/ -v --run-acceptance \
--acceptance-runs=${{ github.event.inputs.acceptance_runs }} --tb=short
fi
env:
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
- name: Create issue on failure
if: failure()
env:
GITEA_API_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: >-
python scripts/create_failure_issue.py
--sha "${{ github.sha }}"
--branch "main"
--run "${{ github.run_number }}"
--message "QE Acceptance Tests Failed"
--workflow "QE Acceptance"
--labels "acceptance-failure,agent-task"
run: |
# Read acceptance report summary if it exists
if [ -f acceptance-report.json ]; then
SUMMARY=$(python -c "import json; r=json.load(open('acceptance-report.json')); print(r.get('final_verdict','?'))")
DETAILS=$(python -c "import json; r=json.load(open('acceptance-report.json')); fd=r.get('failure_details',[]); print('\\n'.join(f'- {d}' for d in fd) if fd else '')")
fi
python scripts/create_failure_issue.py \
--sha "${{ github.sha }}" --branch "main" \
--run "${{ github.run_number }}" \
--message "QE Acceptance: ${SUMMARY:-pipeline failed}" \
--workflow "QE Acceptance" \
--labels "acceptance-failure,agent-task"
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
run: pip install -r requirements.txt
- name: Run tests
run: python -m pytest tests/ -v
run: python -m pytest -v
- name: Create issue on failure
if: failure()
+96 -11
View File
@@ -83,7 +83,7 @@ python scripts/agent_poller.py --action get --issue N
1. git pull origin main
2. git checkout -b dev/issue-N-<slug>
3. 修改功能代码 + 更新/补充 UT 和接口集成测试
4. python -m pytest tests/ -v # 本地全量测试
4. python -m pytest -v # 本地全量测试
5. git commit -m "fix: <描述> - Closes #N"
6. git push origin dev/issue-N-<slug>
```
@@ -94,14 +94,68 @@ python scripts/agent_poller.py --action get --issue N
- 关注 IR 一致性:对同一输入的多次运行结果应尽量稳定
- 关注功能覆盖率:确保 IR 覆盖了输入文档中的功能点
### 4. 等待 CI
### 4. 提交 PR
Push 后 CI 自动运行。可通过 Gitea Actions 页面或 `agent_poller.py` 查看状态。
Push 后立即用 `agent_poller.py` 创建 PR
### 5. 处理结果
```bash
python scripts/agent_poller.py --action create-pr \
--issue N --branch dev/issue-N-<slug> \
--body "## Summary
- <改动摘要>
- **CI 通过**:创建 PR 合并到 main(或直接 push 到 main),`Closes #N` 自动关闭 Issue
- **CI 失败**:CI 自动创建新 Issue,分析失败原因,进入下一轮修复
## Test
- [x] pytest 全量通过 (XX passed, Y skipped)
- [x] UT / 集成测试已更新
Closes #N"
```
PR 创建后,在 Issue 下评论 PR 链接:
```bash
python scripts/agent_poller.py --action comment --issue N \
--body "PR 已创建: <PR_URL>
变更:
- <摘要>
等待 CI 通过后 merge。"
```
### 5. 等待 CI
PR 创建后 CI 自动触发。用 agent_poller 监控状态:
```bash
python scripts/agent_poller.py --action pr-status --pr <PR_NUM>
```
### 6. Merge & 关闭
CI 通过后,执行 merge 并关闭 Issue
```bash
# Merge PR(会自动检查 CI 状态)
python scripts/agent_poller.py --action merge-pr --pr <PR_NUM>
# 如果 Issue 未被自动关闭,手动关闭
python scripts/agent_poller.py --action close-issue --issue N \
--body "PR #<NUM> merged. 变更已合入 main."
```
**一键查看完整生命周期:**
```bash
python scripts/agent_poller.py --action lifecycle --issue N
```
### 7. CI 失败处理
CI 失败时 Gitea 自动创建 `ci-failure` Issue
1. `agent_poller.py --action get --issue <NEW_NUM>` 分析失败原因
2. 在修复分支上修改代码,`git commit --amend` 或新 commit
3. `git push origin dev/issue-N-<slug>` 触发 CI 重跑
4. 重复步骤 5-6 直到 CI 通过
## 闭环
@@ -110,16 +164,47 @@ QE-Agent 开 Issue (qe-feedback)
Dev-Agent 分析 → 开发/重构 → 更新测试
git push → CI (lint + pytest + acceptance)
git push → create-pr → CI (pytest)
┌─ 失败 → 自动开 Issue → 回到开头
┌─ 失败 → 自动开 Issue → push 修复 → 回到 CI
└─ 成功 → Issue 关闭 → QE-Agent 验证 → 新反馈
└─ 成功 → merge-pr → close-issue → QE-Agent 验证 → 新反馈
```
## 提交规范
- **格式**`fix: <简短描述> - Closes #N``feat: <描述> - Closes #N`
- **粒度**:一个 commit 对应一个 Issue
- **测试**:每次提交必须确保 `pytest tests/ -v` 全量通过
- **粒度**:一个 Issue → 一个分支 → 一个 PR → 一个 commit
- **测试**:每次提交必须确保 `python -m pytest -v` 全量通过
- **范围**:不混入与当前 Issue 无关的改动
- **PR**Push 后立即创建 PRCI 通过后 mergePR 信息写入 Issue 后关闭
## agent_poller 命令速查
| 命令 | 用途 | 阶段 |
|------|------|------|
| `--action list` | 列出所有待处理 Issue | 1. 轮询 |
| `--action get --issue N` | 查看 Issue 详情 | 2. 分析 |
| `--action create-pr --issue N --branch X --body "..."` | 创建 PR | 4. 提 PR |
| `--action comment --issue N --body "..."` | 评论 Issue(记录 PR 链接等) | 4. 提 PR |
| `--action pr-status --pr N` | 查看 PR + CI 状态 | 5. 等 CI |
| `--action merge-pr --pr N` | Merge PR(自动检查 CI | 6. Merge |
| `--action close-issue --issue N --body "..."` | 手动关闭 Issue | 6. 关闭 |
| `--action lifecycle --issue N` | 查看 Issue 完整生命周期 | 随时 |
## 闭环完成检查清单
处理每个 Issue 时,确认以下节点全部完成:
- [ ] **分析**`agent_poller.py --action get` 理解 Issue 内容
- [ ] **分支**`git checkout -b dev/issue-N-<slug>`
- [ ] **开发**:修改功能代码 + 同步更新 UT
- [ ] **测试**`python -m pytest -v` 全量通过
- [ ] **提交**`git commit -m "fix: <描述> - Closes #N"`
- [ ] **推送**`git push origin dev/issue-N-<slug>`
- [ ] **PR**`agent_poller.py --action create-pr` 创建 PR
- [ ] **评论**`agent_poller.py --action comment` 在 Issue 下记录 PR 链接
- [ ] **CI**`agent_poller.py --action pr-status` 确认 CI 通过
- [ ] **合并**`agent_poller.py --action merge-pr` 合并 PR
- [ ] **关闭**:确认 Issue 已自动关闭,否则 `--action close-issue`
- [ ] **验证**`agent_poller.py --action lifecycle` 确认全流程完成
+241
View File
@@ -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="<your-token>"
```
GITEA_API_TOKEN 需要 `write:issue``write:repository``write:user` 权限。如果没有设置,从 `config/secrets.yaml` 中读取。
验收测试需要 LLM APILayer 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 <N>
```
分析 issue 描述,确定:
- **测试类型**: 新增验收测试 / 修改已有测试 / 修复测试框架 bug
- **测试位置**: `tests/acceptance/` 下的哪个文件
- **实现方案**: 需要改哪些代码,是否需要新的 fixture 或 schema 规则
在 issue 下评论表示正在处理:
```bash
python scripts/agent_poller.py --action comment --issue <N> --body "QE-Agent 已领取,正在开发测试..."
```
### Step 3: 实施测试
#### 3.1 确保代码最新
```bash
git checkout main
git pull origin main
```
#### 3.2 创建分支
```bash
git checkout -b test/issue-<N>
```
分支命名规则:`test/issue-<N>``test/issue-<N>-<简短描述>`
#### 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 #<N>"
git push origin test/issue-<N>
```
**提交规范**
- 格式:`test: <描述> - Closes #<N>`
- 每个 commit 专注于一个 issue
- 必须包含 `Closes #<N>`(合并后自动关闭 issue
- 不混入无关改动
### Step 5: 创建 PR
```bash
python scripts/agent_poller.py --action create-pr --issue <N> --branch test/issue-<N>
```
PR 标题自动生成为 `fix: <issue title> - Closes #<N>`,描述中包含 `Closes #<N>`
### Step 6: 监控 CI 结果
推送后 CI 自动触发(`ci.yml` push to main / PR to main)。
检查 PR 状态和 CI
```bash
python scripts/agent_poller.py --action pr-status --pr <PR_NUMBER>
```
等待 CI 完成(通常 <2 分钟),根据结果决定下一步:
### Step 7: 处理结果
**CI 通过**
```bash
python scripts/agent_poller.py --action merge-pr --pr <PR_NUMBER>
```
合并后,commit 中的 `Closes #<N>` 会自动关闭对应的 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 <N>
```
确认:
- 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_<name>.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 #<N>` 必须出现在 commit message 中**
5. **本地验证必须通过再 push** — 至少 Layer A + Layer B
6. **如果 Layer CQE Audit)需要验证但 API 不可用** — 在 issue 下评论注明,标记 `--run-acceptance` 通过后 merge
+4
View File
@@ -0,0 +1,4 @@
[pytest]
testpaths = tests skills/ir_generation_skill/tests
python_files = test_*.py
pythonpath = .
+158 -17
View File
@@ -1,10 +1,15 @@
"""Helper for dev agent to interact with Gitea issues.
"""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"
python scripts/agent_poller.py --action lifecycle --issue 2
"""
import argparse
@@ -19,7 +24,6 @@ GITEA_REPO = os.environ.get("GITEA_REPO", "pzhang_zywl/document_analyzer")
GITEA_TOKEN = os.environ.get("GITEA_API_TOKEN", "")
BASE = f"{GITEA_URL}/api/v1/repos/{GITEA_REPO}"
TARGET_LABELS = set() # List all issues, Dev-Agent handles all non-test issues
def _req(method, path, data=None):
@@ -30,21 +34,31 @@ def _req(method, path, data=None):
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
raw = resp.read()
if not raw:
return {} # Gitea merge returns 200 with empty body
return json.loads(raw)
except urllib.error.HTTPError as e:
body = e.read().decode()
print(f"API Error {e.code}: {body}", file=sys.stderr)
sys.exit(1)
def list_issues():
issues = _req("GET", "/issues?state=open")
# ── Issue operations ─────────────────────────────────────────────────────────
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
@@ -65,28 +79,134 @@ def comment_issue(num, body):
return i
def create_pr(issue_num, branch):
# Get issue title for PR description
def close_issue(num, body=None):
"""Close an issue, optionally with a final comment."""
if body:
comment_issue(num, body)
i = _req("PATCH", f"/issues/{num}", {"state": "closed"})
print(f"Issue #{num} closed")
return i
# ── PR operations ────────────────────────────────────────────────────────────
def create_pr(issue_num, branch, body=None):
"""Create a PR for the given issue and branch."""
issue = _req("GET", f"/issues/{issue_num}")
title = f"Fix #{issue_num}: {issue['title'].replace('CI Failure: ', '')}"
body = f"Closes #{issue_num}\n\n{issue.get('body', '')}\n\n🤖 Generated by dev agent"
title = f"fix: {issue['title']} - Closes #{issue_num}"
if body is None:
body = f"Closes #{issue_num}\n\n{issue.get('body', '')}\n\n🤖 Generated by dev agent"
pr = _req("POST", "/pulls", {
"title": title,
"head": branch,
"base": "main",
"body": body,
})
print(f"PR created: {pr.get('html_url', pr.get('url', ''))}")
pr_url = pr.get('html_url', pr.get('url', ''))
print(f"PR created: {pr_url}")
return pr
def pr_status(pr_num):
"""Check PR state and CI status."""
pr = _req("GET", f"/pulls/{pr_num}")
print(f"PR #{pr['number']}: {pr['title']}")
print(f"State: {pr['state']}")
print(f"Merged: {pr.get('merged', False)}")
print(f"Mergeable: {pr.get('mergeable', 'unknown')}")
print(f"URL: {pr['html_url']}")
# CI status
sha = pr.get("head", {}).get("sha", "")
if sha:
try:
status = _req("GET", f"/commits/{sha}/status")
print(f"CI Status: {status.get('state', 'pending')}")
for s in status.get('statuses', []):
desc = s.get('description', '')
print(f" - {s.get('context')}: {s.get('state')} ({desc})")
except SystemExit:
print("CI Status: no statuses found")
return pr
def merge_pr(pr_num):
"""Merge a PR. Fails if CI hasn't passed or PR is not mergeable."""
pr = _req("GET", f"/pulls/{pr_num}")
if pr.get("state") == "closed":
if pr.get("merged"):
print(f"PR #{pr_num} already merged")
return pr
else:
print(f"PR #{pr_num} is closed (not merged)", file=sys.stderr)
sys.exit(1)
# Check CI
sha = pr.get("head", {}).get("sha", "")
ci_passed = True
if sha:
try:
status = _req("GET", f"/commits/{sha}/status")
ci_state = status.get("state", "pending")
if ci_state in ("failure", "error"):
ci_passed = False
print(f"CI status: {ci_state} — cannot merge", file=sys.stderr)
for s in status.get('statuses', []):
print(f" {s.get('context')}: {s.get('state')}", file=sys.stderr)
except SystemExit:
pass # No statuses, proceed
if not ci_passed:
print("Merge blocked: CI has not passed", file=sys.stderr)
sys.exit(1)
result = _req("POST", f"/pulls/{pr_num}/merge", {"Do": "merge"})
# Verify merge success by re-checking PR state
pr_after = _req("GET", f"/pulls/{pr_num}")
if pr_after.get("merged"):
print(f"PR #{pr_num} merged successfully")
elif result.get("merged"):
print(f"PR #{pr_num} merged successfully")
else:
print(f"Merge result: {result.get('message', 'unknown')}")
return result
# ── Lifecycle management ─────────────────────────────────────────────────────
def lifecycle(issue_num):
"""Print the full lifecycle status for an issue: branch, PR, CI, merge."""
print(f"=== Issue #{issue_num} Lifecycle ===\n")
issue = _req("GET", f"/issues/{issue_num}")
print(f"Issue: {issue['title']}")
print(f"State: {issue['state']}\n")
# Find associated PRs
prs = _req("GET", "/pulls?state=all")
related = [p for p in prs if f"Closes #{issue_num}" in p.get("body", "")
or f"#{issue_num}" in p.get("title", "")]
if related:
for pr in related:
print(f"PR #{pr['number']}: {pr['state']} (merged={pr.get('merged', False)})")
pr_status(pr["number"])
else:
print("No associated PR found.")
# ── CLI ──────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Dev agent Gitea helper")
parser.add_argument("--action", required=True,
choices=["list", "get", "comment", "create-pr"])
choices=["list", "get", "comment", "close-issue",
"create-pr", "pr-status", "merge-pr", "lifecycle"])
parser.add_argument("--issue", type=int)
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:
@@ -95,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)
@@ -106,11 +227,31 @@ def main():
print("--issue and --body are required for 'comment' action", file=sys.stderr)
sys.exit(1)
comment_issue(args.issue, args.body)
elif args.action == "close-issue":
if not args.issue:
print("--issue is required for 'close-issue' action", file=sys.stderr)
sys.exit(1)
close_issue(args.issue, args.body)
elif args.action == "create-pr":
if not args.issue or not args.branch:
print("--issue and --branch are required for 'create-pr' action", file=sys.stderr)
sys.exit(1)
create_pr(args.issue, args.branch)
create_pr(args.issue, args.branch, args.body)
elif args.action == "pr-status":
if not args.pr:
print("--pr is required for 'pr-status' action", file=sys.stderr)
sys.exit(1)
pr_status(args.pr)
elif args.action == "merge-pr":
if not args.pr:
print("--pr is required for 'merge-pr' action", file=sys.stderr)
sys.exit(1)
merge_pr(args.pr)
elif args.action == "lifecycle":
if not args.issue:
print("--issue is required for 'lifecycle' action", file=sys.stderr)
sys.exit(1)
lifecycle(args.issue)
if __name__ == "__main__":
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""End-to-end pipeline runner for QE acceptance testing.
Runs the complete document_analyzer pipeline:
1. doc_parser (docx → _parsed.json, if .docx provided)
2. ir_generation steps (parsed JSON → ir_final.json + audit report)
3. QE acceptance tests (optional, if --test flag)
Usage:
python scripts/run_pipeline.py --input <path.docx> # full pipeline
python scripts/run_pipeline.py --parsed <_updated.json> # skip doc_parser
python scripts/run_pipeline.py --parsed <_updated.json> --test # pipeline + acceptance tests
Outputs are placed in output/ matching the project config.py structure:
output/final/ir_final.json
output/final/ir_audit_report.md
acceptance-report.json (if --test)
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
import json
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "skills" / "ir_generation_skill"))
sys.path.insert(0, str(PROJECT_ROOT / "skills" / "doc_parser_skill" / "scripts"))
import config
# ── Stage 1: Document Parsing ────────────────────────────────────────────────
def run_doc_parser(docx_path: str, output_dir: str) -> str | None:
"""Run doc_parser on a .docx file. Returns path to _parsed.json or None."""
from doc_parser import parse_document
print(f"[1/3] Parsing document: {docx_path}")
result = parse_document(docx_path, output_dir, dry_run=False)
parsed_path = result.get("output")
if parsed_path and os.path.isfile(parsed_path):
print(f"{parsed_path}")
return parsed_path
print(" ✗ doc_parser failed to produce output", file=sys.stderr)
return None
# ── Stage 2: IR Generation ───────────────────────────────────────────────────
def run_ir_pipeline(parsed_path: str) -> str | None:
"""Run the ir_generation steps. Returns path to ir_final.json or None."""
config.set_input_file(parsed_path)
os.makedirs(config.PROJECT_OUTPUT, exist_ok=True)
os.makedirs(config.IR_OUTPUT, exist_ok=True)
os.makedirs(config.FINAL_OUTPUT, exist_ok=True)
steps = [
("step1_semantic_index.py", "Semantic Index"),
("step2_ir_extraction.py", "IR Extraction"),
("step2_5_branch_coverage.py", "Branch Coverage"),
("step3_merge_and_audit.py", "Merge & Audit"),
]
print(f"[2/3] Generating IR from: {parsed_path}")
for script, label in steps:
script_path = PROJECT_ROOT / "skills" / "ir_generation_skill" / script
if not script_path.exists():
print(f" ✗ Missing: {script}", file=sys.stderr)
continue
print(f" Running {script} ({label})...")
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=str(PROJECT_ROOT),
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"{script} failed (exit {result.returncode})", file=sys.stderr)
print(result.stderr[-500:], file=sys.stderr)
else:
# Print last line of stdout for brief progress
lines = result.stdout.strip().split("\n")
last = lines[-1] if lines else "done"
print(f"{label}: {last[:120]}")
if os.path.isfile(config.IR_FINAL_JSON):
print(f"{config.IR_FINAL_JSON}")
return config.IR_FINAL_JSON
print(" ✗ IR generation did not produce ir_final.json", file=sys.stderr)
return None
# ── Stage 3: Acceptance Tests ────────────────────────────────────────────────
def run_acceptance_tests() -> int:
"""Run QE acceptance tests. Returns pytest exit code."""
print("[3/3] Running QE acceptance tests...")
test_dir = PROJECT_ROOT / "tests" / "acceptance"
result = subprocess.run(
[
sys.executable, "-m", "pytest", str(test_dir),
"-v", "--run-acceptance",
"--ir-path", config.IR_FINAL_JSON,
"--parsed-path", config.INPUT_JSON,
"--tb=short",
],
cwd=str(PROJECT_ROOT),
)
return result.returncode
# ── Main ─────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Run the full document_analyzer pipeline")
parser.add_argument("--input", help="Path to .docx PRD file")
parser.add_argument("--parsed", help="Path to pre-parsed _updated.json (skip doc_parser)")
parser.add_argument("--test", action="store_true", help="Run acceptance tests after pipeline")
parser.add_argument("--output-dir", default=None, help="Output directory (default: output/)")
args = parser.parse_args()
parsed_path = args.parsed
# Stage 1: doc_parser
if args.input:
docx = args.input
if not os.path.isfile(docx):
print(f"Error: Input file not found: {docx}", file=sys.stderr)
sys.exit(1)
out_dir = args.output_dir or str(PROJECT_ROOT / "output")
parsed_path = run_doc_parser(docx, out_dir)
if not parsed_path:
print("\n✗ Pipeline blocked at Stage 1 (doc_parser)", file=sys.stderr)
# Create tracking issue for dev-agent
_maybe_create_blocking_issue("doc_parser", f"Input: {docx}")
sys.exit(1)
if not parsed_path:
print("Error: Either --input or --parsed is required", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(parsed_path):
print(f"Error: Parsed JSON not found: {parsed_path}", file=sys.stderr)
sys.exit(1)
# Stage 2: IR generation
ir_path = run_ir_pipeline(parsed_path)
if not ir_path:
print("\n✗ Pipeline blocked at Stage 2 (ir_generation)", file=sys.stderr)
_maybe_create_blocking_issue("ir_generation", f"Parsed: {parsed_path}")
sys.exit(1)
print(f"\n✓ Pipeline complete: {ir_path}")
# Stage 3: Acceptance tests
if args.test:
exit_code = run_acceptance_tests()
sys.exit(exit_code)
def _maybe_create_blocking_issue(stage: str, detail: str):
"""Notify about a pipeline blockage. The acceptance CI will create the issue."""
print(f"\n⚠ Stage '{stage}' failed. CI will create an acceptance-failure issue.", file=sys.stderr)
if __name__ == "__main__":
main()
+3 -2
View File
@@ -26,19 +26,20 @@ case "$MODE" in
echo ""
echo "正在执行单次检查..."
claude -p --agent agents/DEV_AGENT.md \
"你是 Dev-Agent检查 Gitea 所有打开的 Issue,跳过纯测试相关的,其他全部领取分析并修复,记得同步更新测试。"
"你是 Dev-Agent检查 Gitea 所有打开的 Issue--action list),跳过纯测试相关的。对每个负责的 Issue,走完完整闭环:分析 → 分支 → 开发+UT → pytest → commit → push → create-pr → comment Issue → 等 CI → merge-pr → 关闭。"
;;
2)
echo ""
echo "启动持续轮询模式 (每 10 分钟)..."
echo "按 Ctrl+C 停止"
claude -p --agent agents/DEV_AGENT.md \
"你是 Dev-Agent用 loop 模式每 10 分钟检查一次 Gitea 所有打开的 Issue,跳过纯测试相关的,其他全部领取处理。完成后评论进度,push 触发 CI。"
"你是 Dev-Agent用 loop 模式每 10 分钟检查一次 Gitea Issue--action list)。跳过纯测试相关的。每个 Issue 走完整闭环:分析→开发→push→create-pr→comment→CI→merge-pr→close。每个步骤用 agent_poller.py 对应命令。"
;;
3)
echo ""
echo "启动交互模式..."
echo "进入后输入: 检查 Gitea Issues 并处理"
echo "可用命令速查: agent_poller.py --help"
claude --agent agents/DEV_AGENT.md
;;
*)
+53
View File
@@ -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 相关 Issuemain 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 <N>"
echo " python -m pytest tests/acceptance/ -v --run-acceptance"
claude --agent agents/QE_AGENT.md
;;
*)
echo "无效选择。"
exit 1
;;
esac
+43 -19
View File
@@ -4,23 +4,29 @@ Reads API keys from a secrets.yaml file, falling back to environment variables.
"""
import os
import sys
import json
import yaml
# ---- Paths ----
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
WORKSPACE_DIR = os.path.dirname(BASE_DIR)
PROJECT_ROOT = os.path.dirname(WORKSPACE_DIR)
PROJECT_OUTPUT = os.path.join(PROJECT_ROOT, "output")
# Subdirectories under PROJECT_OUTPUT
IR_OUTPUT = os.path.join(PROJECT_OUTPUT, "ir")
FINAL_OUTPUT = os.path.join(PROJECT_OUTPUT, "final")
# Legacy paths (maintained for doc_parser integration)
DOC_PARSER_OUTPUT = os.path.join(WORKSPACE_DIR, "doc_parser_skill", "output")
PROMPTS_DIR = os.path.join(BASE_DIR, "prompts")
TESTS_DIR = os.path.join(BASE_DIR, "tests")
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
OUTPUT_DIR = IR_OUTPUT # backward compatibility alias
# Input file (the parsed PRD JSON)
_DEFAULT_INPUT = os.path.join(
DOC_PARSER_OUTPUT,
"车机娱乐系统禁止功能文档_脱敏 v0.9_v2_updated.json",
)
INPUT_JSON = os.environ.get("IR_INPUT_JSON", _DEFAULT_INPUT)
# Input file (the parsed PRD JSON) — must be set via env var or CLI
# No hardcoded default to avoid silently processing the wrong document.
INPUT_JSON = os.environ.get("IR_INPUT_JSON", None)
def set_input_file(path: str) -> None:
@@ -35,18 +41,18 @@ SECRETS_YAML = os.path.join(
OPENCLAW_HOME, "workspace-document-analyzer", "config", "secrets.yaml",
)
# Intermediate outputs
SEMANTIC_INDEX_R1_JSON = os.path.join(OUTPUT_DIR, "semantic_index_r1.json")
SEMANTIC_INDEX_R2_JSON = os.path.join(OUTPUT_DIR, "semantic_index_r2.json")
SEMANTIC_INDEX_R3_JSON = os.path.join(OUTPUT_DIR, "semantic_index_r3.json")
SEMANTIC_INDEX_JSON = os.path.join(OUTPUT_DIR, "semantic_index.json") # merged final
IR_FRAGMENTS_JSON = os.path.join(OUTPUT_DIR, "ir_fragments.json")
PATH_ENUM_JSON = os.path.join(OUTPUT_DIR, "path_enumeration.json")
IR_AUTOCOMPLETE_FRAGMENTS_JSON = os.path.join(OUTPUT_DIR, "ir_autocomplete_fragments.json")
# Intermediate outputs (all under PROJECT_OUTPUT/ir/)
SEMANTIC_INDEX_R1_JSON = os.path.join(IR_OUTPUT, "semantic_index_r1.json")
SEMANTIC_INDEX_R2_JSON = os.path.join(IR_OUTPUT, "semantic_index_r2.json")
SEMANTIC_INDEX_R3_JSON = os.path.join(IR_OUTPUT, "semantic_index_r3.json")
SEMANTIC_INDEX_JSON = os.path.join(IR_OUTPUT, "semantic_index.json")
IR_FRAGMENTS_JSON = os.path.join(IR_OUTPUT, "ir_fragments.json")
PATH_ENUM_JSON = os.path.join(IR_OUTPUT, "path_enumeration.json")
IR_AUTOCOMPLETE_FRAGMENTS_JSON = os.path.join(IR_OUTPUT, "ir_autocomplete_fragments.json")
# Final deliverables (placed in doc_parser output per spec)
IR_FINAL_JSON = os.path.join(DOC_PARSER_OUTPUT, "ir_final.json")
IR_AUDIT_REPORT_MD = os.path.join(DOC_PARSER_OUTPUT, "ir_audit_report.md")
# Final deliverables (under PROJECT_OUTPUT/final/)
IR_FINAL_JSON = os.path.join(FINAL_OUTPUT, "ir_final.json")
IR_AUDIT_REPORT_MD = os.path.join(FINAL_OUTPUT, "ir_audit_report.md")
# ---- LLM API ----
# Choose provider: "deepseek" | "dashscope"
@@ -118,8 +124,26 @@ def llm_client():
def load_input_document(path: str | None = None) -> dict:
"""Load the parsed PRD JSON document."""
"""Load the parsed PRD JSON document.
Args:
path: Explicit file path. If None, reads from IR_INPUT_JSON env var.
Raises:
FileNotFoundError: If no path is configured.
SystemExit: If the configured path does not exist.
"""
path = path or INPUT_JSON
if not path:
print("错误: 未指定输入文件。请通过以下任一方式指定:", file=sys.stderr)
print(" 1. 设置环境变量: IR_INPUT_JSON=<path>", file=sys.stderr)
print(" 2. 通过 main.py: python main.py --input <path>", file=sys.stderr)
print(" 3. 通过 step 脚本: python step1_semantic_index.py --input <path>", file=sys.stderr)
print(" 4. 程序调用: config.set_input_file(<path>)", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(path):
print(f"错误: 输入文件不存在: {path}", file=sys.stderr)
sys.exit(1)
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
@@ -365,6 +365,97 @@ def run_all_tests():
return total_failures == 0
# ═══════════════════════════════════════════════════════════════════════════════
# pytest discovery support — skips gracefully when output files are absent
# ═══════════════════════════════════════════════════════════════════════════════
import pytest # noqa: E402
def _load_si_and_doc():
"""Try to load semantic_index.json and the input document. Returns (si, doc) or (None, None)."""
try:
si = config.load_json(config.SEMANTIC_INDEX_JSON)
doc = config.load_input_document()
return si, doc
except FileNotFoundError:
return None, None
def test_step1_unit_ids():
"""pytest: verify all function_units have valid unit_id and name."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found — run step1_semantic_index.py first")
units = si.get("function_units", [])
errors = check_unit_ids(units)
assert not errors, f"unit_id/name errors: {errors}"
def test_step1_path_fields():
"""pytest: verify all function_units have non-empty path arrays."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found")
units = si.get("function_units", [])
errors = check_unit_paths(units)
assert not errors, f"path field errors: {errors}"
def test_step1_concept_parents():
"""pytest: verify concept parent references are valid."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found")
concepts = si.get("concepts", [])
errors = check_concept_parents(concepts)
assert not errors, f"concept parent errors: {errors}"
def test_step1_sources_exist():
"""pytest: verify all source references point to real content."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found")
units = si.get("function_units", [])
image_index = build_image_index(doc)
node_index = build_logic_tree_node_index(doc)
errors = check_sources_exist(units, image_index, node_index)
assert not errors, f"source reference errors: {errors}"
def test_step1_logic_tree_coverage():
"""pytest: verify decision/action nodes in logic trees are covered (warnings only)."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found")
units = si.get("function_units", [])
node_index = build_logic_tree_node_index(doc)
warnings = check_logic_tree_coverage(units, node_index)
# Warnings are informational, not failures — but report them
if warnings:
print(f"\n[WARN] Logic tree coverage warnings: {warnings}")
def test_step1_ensemble_confidence():
"""pytest: verify function_units have confidence/ensemble_support/source_versions."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found")
units = si.get("function_units", [])
errors = check_ensemble_confidence(units)
assert not errors, f"ensemble confidence errors: {errors}"
def test_step1_confidence_summary():
"""pytest: verify confidence_summary counts match actual unit/concept counts."""
si, doc = _load_si_and_doc()
if si is None:
pytest.skip("semantic_index.json not found")
errors = check_confidence_summary(si)
assert not errors, f"confidence_summary errors: {errors}"
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)
@@ -317,6 +317,93 @@ def run_all_tests():
return total_failures == 0
# ═══════════════════════════════════════════════════════════════════════════════
# pytest discovery support
# ═══════════════════════════════════════════════════════════════════════════════
import pytest # noqa: E402
def _load_fragments_or_skip():
"""Load ir_fragments.json or return None."""
try:
return config.load_json(config.IR_FRAGMENTS_JSON)
except FileNotFoundError:
return None
def test_step2_non_empty_rules():
"""pytest: every fragment must have at least one rule."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found — run step2_ir_extraction.py first")
errors = check_non_empty_rules(fragments)
assert not errors, f"non-empty rule errors: {errors}"
def test_step2_rule_paths():
"""pytest: every rule must have a non-empty path array."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_rule_paths(fragments)
assert not errors, f"rule path errors: {errors[:5]}"
def test_step2_precondition_fields():
"""pytest: every rule must have precondition with geographic_scope and screen_type."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_precondition_fields(fragments)
assert not errors, f"precondition errors: {errors[:5]}"
def test_step2_user_interaction_content():
"""pytest: user_interaction actions must have non-empty, non-placeholder content."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_user_interaction_content(fragments)
assert not errors, f"user_interaction content errors: {errors[:5]}"
def test_step2_sources_have_refs():
"""pytest: every rule should reference at least one source."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_sources_have_logic_tree_nodes(fragments)
assert not errors, f"source reference errors: {errors[:5]}"
def test_step2_trigger_conditions():
"""pytest: every trigger condition must have signal, operator, value."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_trigger_conditions(fragments)
assert not errors, f"trigger condition errors: {errors[:5]}"
def test_step2_duplicate_rule_ids():
"""pytest: no duplicate rule_ids across all fragments."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_duplicate_rule_ids(fragments)
assert not errors, f"duplicate rule_id errors: {errors}"
def test_step2_action_types():
"""pytest: all actions must have valid types."""
fragments = _load_fragments_or_skip()
if fragments is None:
pytest.skip("ir_fragments.json not found")
errors = check_action_types(fragments)
assert not errors, f"action type errors: {errors[:5]}"
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)
@@ -147,6 +147,32 @@ def run_all_tests():
return total_failures == 0
# ═══════════════════════════════════════════════════════════════════════════════
# pytest discovery support
# ═══════════════════════════════════════════════════════════════════════════════
import pytest # noqa: E402
def test_step2_5_path_enumeration():
"""pytest: path_enumeration.json should have valid structure."""
try:
path_data = config.load_json(config.PATH_ENUM_JSON)
except FileNotFoundError:
pytest.skip("path_enumeration.json not found — run step2_5_branch_coverage.py first")
errors = check_path_enumeration(path_data)
assert not errors, f"path enumeration errors: {errors}"
def test_step2_5_autocomplete_fragments():
"""pytest: auto-complete fragments must have valid structure if present."""
fragments = load_autocomplete_fragments()
errors = check_autocomplete_fragments(fragments)
# If fragments is None (no autocomplete needed), check_autocomplete_fragments returns a warning
actual_errors = [e for e in errors if "未生成" not in e]
assert not actual_errors, f"autocomplete fragment errors: {actual_errors}"
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)
@@ -227,6 +227,77 @@ def run_all_tests():
return total_failures == 0
# ═══════════════════════════════════════════════════════════════════════════════
# pytest discovery support
# ═══════════════════════════════════════════════════════════════════════════════
import pytest # noqa: E402
def _load_ir_final_or_skip():
"""Load ir_final.json or return None."""
try:
return config.load_json(config.IR_FINAL_JSON)
except FileNotFoundError:
return None
def _load_audit_report_or_skip():
"""Load ir_audit_report.md or return None."""
try:
with open(config.IR_AUDIT_REPORT_MD, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return None
def test_step3_top_level_structure():
"""pytest: ir_final must have required top-level fields."""
ir = _load_ir_final_or_skip()
if ir is None:
pytest.skip("ir_final.json not found — run step3_merge_and_audit.py first")
errors = check_top_level_structure(ir)
assert not errors, f"top-level structure errors: {errors}"
def test_step3_rule_ids():
"""pytest: rule_ids must be unique and follow naming convention."""
ir = _load_ir_final_or_skip()
if ir is None:
pytest.skip("ir_final.json not found")
errors = check_rule_ids(ir)
assert not errors, f"rule_id errors: {errors[:5]}"
def test_step3_rule_paths():
"""pytest: every rule must have a non-empty path array."""
ir = _load_ir_final_or_skip()
if ir is None:
pytest.skip("ir_final.json not found")
rules = ir.get("rules", [])
errors = check_rule_paths(rules)
assert not errors, f"rule path errors: {errors[:5]}"
def test_step3_rule_completeness():
"""pytest: each rule must have all required fields."""
ir = _load_ir_final_or_skip()
if ir is None:
pytest.skip("ir_final.json not found")
rules = ir.get("rules", [])
errors = check_rule_completeness(rules)
assert not errors, f"rule completeness errors: {errors[:5]}"
def test_step3_audit_report():
"""pytest: audit report must have all required sections."""
report = _load_audit_report_or_skip()
if report is None:
pytest.skip("ir_audit_report.md not found — run step3_merge_and_audit.py first")
errors = check_audit_report(report)
assert not errors, f"audit report errors: {errors[:5]}"
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)
+46 -26
View File
@@ -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 {}
@@ -115,10 +131,7 @@ def ir_path(request) -> str:
path = (
request.config.getoption("--ir-path")
or os.environ.get("TEST_IR_PATH")
or str(
Path.home()
/ ".openclaw/workspace/skills/doc_parser_skill/output/ir_final.json"
)
or str(_PROJECT_ROOT / "output" / "final" / "ir_final.json")
)
if not os.path.exists(path):
pytest.skip(f"IR file not found: {path}")
@@ -139,8 +152,7 @@ def parsed_path(request) -> str | None:
request.config.getoption("--parsed-path")
or os.environ.get("TEST_PARSED_PATH")
or str(
_PROJECT_ROOT
/ "skills/ir_generation_skill/车机娱乐系统禁止功能文档_精简_updated.json"
_PROJECT_ROOT / "output" / "车机娱乐系统禁止功能文档_精简_updated.json"
)
)
if os.path.exists(path):
@@ -182,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
@@ -258,21 +272,27 @@ def acceptance_runs(request) -> int:
def run_ir_pipeline():
"""Return a callable that runs the IR generation pipeline on a parsed JSON.
Returns None if the pipeline script is not available in the current environment.
This is common when the acceptance tests run on pre-generated IR output.
Usage::
ir_data, ir_path = run_ir_pipeline(parsed_json_path, output_dir)
runner = run_ir_pipeline()
if runner:
ir_data, ir_path = runner(parsed_json_path, output_dir)
"""
sys.path.insert(0, _skill_path("ir_generation_skill"))
ir_gen_path = (
_PROJECT_ROOT / "skills" / "ir_generation_skill" / "scripts" / "ir_generator.py"
)
if not ir_gen_path.exists():
return None
sys.path.insert(0, str(ir_gen_path.parent))
from ir_generator import generate_ir
def _run(parsed_path: str, output_dir: str | None = None) -> tuple[dict, str]:
"""Run IR generation and return (ir_data, ir_path)."""
def _run(parsed_path: str, output_dir: str | None = None) -> tuple[list, str]:
out = output_dir or tempfile.mkdtemp(prefix="qe_acceptance_")
result = generate_ir(parsed_path, out, dry_run=False)
ir_list = result.get("ir", [])
ir_path = result.get("path", "")
# ir_generator produces a list; wrap to match rich format expectations
# for schema validation we accept both formats
return ir_list, ir_path
return result.get("ir", []), result.get("path", "")
return _run
+27 -11
View File
@@ -57,18 +57,28 @@ def test_layer_a_schema(ir_data: dict, request):
NON_FUNCTIONAL_PATTERNS = [
re.compile(p) for p in [
r"编制.*变更.*日志",
r"变更日志",
r"文档背景",
r"文档范围",
r"术语解释",
r"参考",
r"参考(文献|文档|资料)?",
r"附录",
r"版本",
r"变更记录",
r"目录",
r"前言",
r"概述",
r"简介",
r"概述.*背景",
r"产品简介",
r"场景.*(说明|概述)",
r".*概要说明$",
r"相关文档",
r"行业规范",
r"政策法规",
r"非功能说明",
r"背景介绍",
r"PRD", # document title like "XX Auto XXX PRD V1.0"
r"产品架构", # architecture overview
r"系统架构",
]
]
@@ -76,15 +86,20 @@ NON_FUNCTIONAL_PATTERNS = [
def _is_functional_section(section_name: str) -> bool:
"""Heuristic: exclude background, glossary, changelog, scope sections.
Sections that are purely structural — preface, glossary, changelog — are excluded.
Sections with numbering like '3.1.1' are always considered functional.
Check non-functional patterns first, then treat numbered sections (like
'3.1.1 系统限制') as likely functional.
"""
# Numbered sections are functional
if _section_number(section_name) != section_name:
return True
# Explicitly non-functional patterns (checked first)
for pat in NON_FUNCTIONAL_PATTERNS:
if pat.search(section_name):
return False
# Documents with only a title (no section number) — check for functional keywords
sec_num = _section_number(section_name)
if "." not in sec_num and not sec_num[0].isdigit():
func_keywords = ["策略", "规则", "功能", "限制", "流程", "配置", "场景",
"约束", "条件", "方案", "逻辑", "处理", "机制", "禁止"]
if not any(kw in section_name for kw in func_keywords):
return False
return True
@@ -263,19 +278,20 @@ def test_layer_b_coverage(
stability_values: list[float] = [cov["overall_rate"]]
stability_std = 0.0
if acceptance_runs > 1:
if acceptance_runs > 1 and run_ir_pipeline is not None:
parsed_path = request.config.getoption("--parsed-path")
if parsed_path and os.path.exists(parsed_path):
for _ in range(acceptance_runs - 1):
try:
ir_list, _ = run_ir_pipeline(parsed_path)
# Convert list-format IR to dict for coverage measurement
run_ir = _wrap_list_ir(ir_list)
run_cov = _measure_coverage(run_ir, parsed_data)
stability_values.append(run_cov["overall_rate"])
time.sleep(0.5) # rate limiting between runs
time.sleep(0.5)
except Exception as e:
pytest.fail(f"Stability run failed: {e}")
elif acceptance_runs > 1 and run_ir_pipeline is None:
print(" [Layer B] Stability testing skipped: pipeline runner not available")
if len(stability_values) > 1:
stability_std = statistics.stdev(stability_values)
+106
View File
@@ -0,0 +1,106 @@
"""Unit tests for config.py pure functions."""
import os
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "ir_generation_skill"))
import config
def test_set_input_file():
original = config.INPUT_JSON
try:
config.set_input_file("/tmp/test_input.json")
assert config.INPUT_JSON == "/tmp/test_input.json"
finally:
config.set_input_file(original)
def test_no_hardcoded_input_file():
"""INPUT_JSON should not have a hardcoded default — comes from env or None."""
# When IR_INPUT_JSON env is not set, INPUT_JSON should be None
env_val = os.environ.pop("IR_INPUT_JSON", None)
try:
import importlib
importlib.reload(config)
# After reload without env var, INPUT_JSON should be None
assert config.INPUT_JSON is None, \
f"INPUT_JSON should be None when env not set, got: {config.INPUT_JSON}"
finally:
if env_val is not None:
os.environ["IR_INPUT_JSON"] = env_val
importlib.reload(config)
def test_set_input_file_accepts_none():
"""set_input_file(None) should work for resetting."""
original = config.INPUT_JSON
try:
config.set_input_file("/tmp/test.json")
config.set_input_file(None)
assert config.INPUT_JSON is None
finally:
config.set_input_file(original)
def test_config_constants_exist():
"""Verify all expected path constants are defined."""
assert config.BASE_DIR
assert config.PROJECT_ROOT
assert config.PROJECT_OUTPUT
assert config.IR_OUTPUT
assert config.FINAL_OUTPUT
assert config.OUTPUT_DIR
assert config.PROMPTS_DIR
assert config.TESTS_DIR
assert config.DOC_PARSER_OUTPUT
assert config.SEMANTIC_INDEX_JSON
assert config.IR_FRAGMENTS_JSON
assert config.PATH_ENUM_JSON
assert config.IR_AUTOCOMPLETE_FRAGMENTS_JSON
assert config.IR_FINAL_JSON
assert config.IR_AUDIT_REPORT_MD
def test_output_dir_is_under_project_root():
"""PROJECT_OUTPUT, IR_OUTPUT, FINAL_OUTPUT should all be under PROJECT_ROOT."""
assert config.PROJECT_OUTPUT.startswith(config.PROJECT_ROOT)
assert config.IR_OUTPUT.startswith(config.PROJECT_OUTPUT)
assert config.FINAL_OUTPUT.startswith(config.PROJECT_OUTPUT)
def test_output_dir_structure():
"""IR files should go to output/ir/, final deliverable to output/final/."""
assert config.IR_OUTPUT.endswith(os.path.join("output", "ir"))
assert config.FINAL_OUTPUT.endswith(os.path.join("output", "final"))
assert config.SEMANTIC_INDEX_JSON.startswith(config.IR_OUTPUT)
assert config.IR_FRAGMENTS_JSON.startswith(config.IR_OUTPUT)
assert config.IR_FINAL_JSON.startswith(config.FINAL_OUTPUT)
assert config.IR_AUDIT_REPORT_MD.startswith(config.FINAL_OUTPUT)
def test_ensemble_temperatures_count():
"""Should have exactly 3 ensemble temperatures."""
assert len(config.ENSEMBLE_TEMPERATURES) == 3
def test_max_tokens_is_int():
assert isinstance(config.MAX_TOKENS, int)
assert config.MAX_TOKENS > 0
def test_temperature_is_float():
assert isinstance(config.TEMPERATURE, float)
assert 0.0 <= config.TEMPERATURE <= 2.0
def test_provider_models_has_expected_keys():
assert "deepseek" in config.PROVIDER_MODELS
assert "dashscope" in config.PROVIDER_MODELS
def test_model_name_in_provider_models():
assert config.MODEL_NAME in config.PROVIDER_MODELS.values()
+200
View File
@@ -0,0 +1,200 @@
"""Unit tests for pure functions in detect_conflicts.py — no LLM calls, no file I/O."""
import json
import sys
from pathlib import Path
import pytest
# Add detect_conflicts script to path
sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "conflict_detection_skill" / "scripts"))
import detect_conflicts as dc
# ── _is_nested_tree ──────────────────────────────────────────────────────────
def test_is_nested_tree_with_children_list():
assert dc._is_nested_tree({"children": []}) is True
def test_is_nested_tree_without_children_list():
assert dc._is_nested_tree({"nodes": []}) is False
def test_is_nested_tree_empty_dict():
assert dc._is_nested_tree({}) is False
# ── _nested_tree_to_text ─────────────────────────────────────────────────────
def test_nested_tree_simple():
tree = {
"id": "N1",
"name": "开始",
"type": "start",
"children": [
{
"id": "N2",
"name": "判断条件",
"type": "decision",
"children": [
{
"condition": "",
"node": {
"id": "N3",
"name": "执行动作",
"type": "action",
},
},
{
"condition": "",
"node": {
"id": "N4",
"name": "结束",
"type": "end",
},
},
],
},
],
}
result = dc._nested_tree_to_text(tree)
assert "[起始] N1: 开始" in result
assert "[判断] N2: 判断条件" in result
assert '分支 "":' in result
assert "[动作] N3: 执行动作" in result
assert '分支 "":' in result
assert "[结束] N4: 结束" in result
def test_nested_tree_empty_children():
tree = {"id": "N1", "name": "结束", "type": "end"}
result = dc._nested_tree_to_text(tree)
assert "[结束] N1: 结束" in result
# ── _flat_tree_to_text ───────────────────────────────────────────────────────
def test_flat_tree_with_decision_and_action():
lt = {
"root": "START",
"nodes": [
{"id": "D1", "type": "decision", "condition": "车速>15",
"branches": [{"value": "", "target": "A1"}, {"value": "", "target": "END"}]},
{"id": "A1", "type": "action", "description": "禁止视频播放"},
{"id": "END", "type": "end", "description": "结束"},
],
}
result = dc._flat_tree_to_text(lt)
assert "根节点: START" in result
assert '判断节点 D1: 条件="车速>15"' in result
assert '分支 "" → A1' in result
assert "动作节点 A1: 禁止视频播放" in result
def test_flat_tree_empty():
result = dc._flat_tree_to_text({})
assert result == ""
# ── _logic_tree_to_text (dispatcher) ─────────────────────────────────────────
def test_logic_tree_to_text_nested():
lt = {"children": [{"id": "N1", "name": "开始", "type": "start"}]}
result = dc._logic_tree_to_text(lt)
assert "[起始] N1: 开始" in result
def test_logic_tree_to_text_flat():
lt = {"nodes": [{"id": "A1", "type": "action", "description": "测试"}]}
result = dc._logic_tree_to_text(lt)
assert "动作节点 A1: 测试" in result
# ── _parse_conflict_json ─────────────────────────────────────────────────────
def test_parse_no_conflict():
assert dc._parse_conflict_json("[[NO_CONFLICT]]") == []
assert dc._parse_conflict_json("some text [[NO_CONFLICT]] more") == []
def test_parse_conflict_valid_json():
content = json.dumps([
{"conflict_type": "condition_mismatch", "severity": "high",
"section": "3.1", "image_snippet": "img", "text_snippet": "txt",
"description": "冲突描述"}
], ensure_ascii=False)
result = dc._parse_conflict_json(content)
assert len(result) == 1
assert result[0]["conflict_type"] == "condition_mismatch"
def test_parse_conflict_with_markdown_fence():
content = '```json\n[{"conflict_type": "contradiction", "severity": "high", "section": "1.0", "image_snippet": "a", "text_snippet": "b", "description": "x"}]\n```'
result = dc._parse_conflict_json(content)
assert len(result) == 1
assert result[0]["conflict_type"] == "contradiction"
def test_parse_conflict_unparseable():
assert dc._parse_conflict_json("not valid json at all") == []
assert dc._parse_conflict_json("") == []
def test_parse_conflict_mixed_content():
content = 'some prefix text\n[\n {"conflict_type": "scope_mismatch", "severity": "low", "section": "4.2", "image_snippet": "img", "text_snippet": "txt", "description": "d"}]\nsuffix'
result = dc._parse_conflict_json(content)
assert len(result) == 1
assert result[0]["conflict_type"] == "scope_mismatch"
# ── _build_text_for_section ──────────────────────────────────────────────────
def test_build_text_para_blocks():
sections = [
{"source": "3.1 测试章节",
"blocks": [
{"type": "para", "text": "第一段文字"},
{"type": "para", "text": "第二段文字"},
]},
]
result = dc._build_text_for_section(sections, "3.1 测试章节")
assert "第一段文字" in result
assert "第二段文字" in result
def test_build_text_table_blocks():
sections = [
{"source": "4.0 功能列表",
"blocks": [
{"type": "table", "table": "功能表",
"rows": [
{"columns": [{"name": "功能", "text": "视频播放"}, {"name": "状态", "text": "禁止"}]},
]},
]},
]
result = dc._build_text_for_section(sections, "4.0 功能列表")
assert "功能表" in result
assert "视频播放" in result
assert "禁止" in result
def test_build_text_section_not_found():
sections = [{"source": "1.0 概述", "blocks": []}]
result = dc._build_text_for_section(sections, "2.0 不存在的章节")
assert result == ""
def test_build_text_mixed_blocks():
sections = [
{"source": "5.1 混合章节",
"blocks": [
{"type": "para", "text": "介绍文字"},
{"type": "table", "table": "表1",
"rows": [{"columns": [{"name": "A", "text": "值1"}]}]},
]},
]
result = dc._build_text_for_section(sections, "5.1 混合章节")
assert "介绍文字" in result
assert "表1" in result
assert "值1" in result
+1 -1
View File
@@ -83,7 +83,7 @@ def test_sample_ir_json_is_valid():
"""The sample IR JSON file should be valid JSON."""
sample_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"skills", "ir_generation_skill",
"output",
"车机娱乐系统禁止功能文档_精简_updated.json"
)
if os.path.exists(sample_path):