Files
sanguo_moziplus_v2/docs/design/18-test-design.md
T
cfdaily f55a037c98
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 28s
CI / notify-on-failure (pull_request) Successful in 1s
[moz] docs: §18 API 聚合重构 + 工具链 Tab 设计文档
- 18-api-refactor-and-toolchain-tab.md: 主设计(9章+实施约束)
  - 后端拆分方案 B(task_routes + task_relation_routes + shared)
  - expand 细粒度聚合(comments/events 带 limit+total_count)
  - 任务列表搜索参数 q
  - 工具链 Tab 设计(仿 MailPanel + 搜索栏)
  - GATE 门控 + 委派原则 + wiki 查询规则
  - 司马懿已审(mail-1781415763066)
- 18-test-design.md: 测试用例详细设计(34 个用例 + CI 集成)
- tests/scripts/verify_api_compat.sh: 路由兼容性验证脚本
2026-06-14 13:53:56 +08:00

485 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# §18 测试用例详细设计
> **关联**: `docs/design/18-api-refactor-and-toolchain-tab.md`
> **日期**: 2026-06-14
---
## 1. 测试文件规划
| 文件 | 类型 | 测试数 | 说明 |
|------|------|--------|------|
| `tests/integration/test_api.py` | 集成 | 扩展现有 | 拆分后回归验证 |
| `tests/unit/test_task_routes.py` | 单元 | 14 | task_routes 专项 |
| `tests/unit/test_expand_api.py` | 单元 | 7 | expand 聚合专项 |
| `tests/unit/test_task_search.py` | 单元 | 4 | 搜索专项 |
| `tests/scripts/verify_api_compat.sh` | 脚本 | 1 | CI 路由兼容性 |
**总计**:26 个测试 + 1 个 CI 脚本
---
## 2. 预置 Fixture
```python
# tests/conftest.py 新增(如果不存在则补充)
@pytest.fixture
def expand_env(tmp_path):
"""expand 测试环境:1 个 task + 预置关联数据"""
project_root = tmp_path / "projects"
project_root.mkdir()
os.environ["BLACKBOARD_ROOT"] = str(project_root)
reg = ProjectRegistry(project_root)
reg.create_project("test-proj", "Test Project", agents=["agent1"])
bb = Blackboard(project_root / "test-proj" / "blackboard.db")
bb.create_task(Task(id="t1", title="Expand Test Task", task_type="coding"))
# 预置 25 条 comment
for i in range(25):
bb.add_comment("t1", agent="agent1", comment_type="general",
content=f"Comment number {i}")
# 预置 5 条 output
for i in range(5):
bb.write_output("t1", agent="agent1",
output_type="code",
content=f"output content {i}",
filename=f"file_{i}.py")
# 预置 3 条 review
for i in range(3):
bb.add_review("t1", reviewer="agent1",
verdict="APPROVE",
confidence=0.9,
risk_level="low",
summary=f"Review {i}")
# 预置 2 条 decision
for i in range(2):
bb.add_decision("t1", agent="agent1",
decision_type="scope",
rationale=f"Decision {i}")
# 预置 35 条 event
from src.blackboard.queries import Queries
q = Queries(project_root / "test-proj" / "blackboard.db")
for i in range(35):
q.add_event("t1", event_type="status_change",
detail=f"Event {i}")
yield project_root
del os.environ["BLACKBOARD_ROOT"]
```
---
## 3. task_routes.py 测试(test_task_routes.py
```python
"""task_routes.py 路由测试 — 验证拆分后行为不变"""
import pytest
from fastapi.testclient import TestClient
from src.main import app
client = TestClient(app)
pytestmark = pytest.mark.integration
class TestTaskListRoutes:
"""GET /tasks + 搜索"""
def test_list_tasks_basic(self, project_env):
"""列表基本返回格式不变"""
resp = client.get("/api/projects/test-proj/tasks")
assert resp.status_code == 200
data = resp.json()
assert "tasks" in data
assert isinstance(data["tasks"], list)
def test_list_tasks_with_search(self, project_env):
"""q 参数搜索标题"""
resp = client.get("/api/projects/test-proj/tasks?q=Existing")
data = resp.json()
assert len(data["tasks"]) == 1
assert "Existing" in data["tasks"][0]["title"]
def test_list_tasks_search_case_insensitive(self, project_env):
"""大小写不敏感"""
resp = client.get("/api/projects/test-proj/tasks?q=existing")
data = resp.json()
assert len(data["tasks"]) == 1
def test_list_tasks_search_no_match(self, project_env):
"""无匹配返回空列表"""
resp = client.get("/api/projects/test-proj/tasks?q=nonexistent_xyz")
data = resp.json()
assert len(data["tasks"]) == 0
def test_list_tasks_search_empty_q(self, project_env):
"""q 为空返回全部"""
resp = client.get("/api/projects/test-proj/tasks?q=")
data = resp.json()
assert len(data["tasks"]) >= 1
class TestTaskDetailRoutes:
"""GET /tasks/{tid} + expand"""
def test_get_task_basic(self, project_env):
"""无 expand 返回基本 task"""
resp = client.get("/api/projects/test-proj/tasks/t1")
assert resp.status_code == 200
data = resp.json()
assert data["id"] == "t1"
assert "comments" not in data # 无 expand 不含关联数据
def test_get_task_404(self, project_env):
"""不存在的 task 返回 404"""
resp = client.get("/api/projects/test-proj/tasks/nonexistent")
assert resp.status_code == 404
class TestTaskActionRoutes:
"""claim/status/patch/archive 行为不变"""
def test_claim_task(self, project_env):
"""认领行为不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/claim",
json={"agent": "agent1"})
assert resp.status_code == 200
def test_update_status(self, project_env):
"""状态流转不变"""
client.post("/api/projects/test-proj/tasks/t1/claim",
json={"agent": "agent1"})
resp = client.post("/api/projects/test-proj/tasks/t1/status",
json={"agent": "agent1", "status": "working"})
assert resp.status_code == 200
def test_invalid_status_transition(self, project_env):
"""非法状态转换返回 409"""
resp = client.post("/api/projects/test-proj/tasks/t1/status",
json={"agent": "agent1", "status": "done"})
assert resp.status_code == 409
def test_patch_task(self, project_env):
"""PATCH 更新不变"""
resp = client.patch("/api/projects/test-proj/tasks/t1",
json={"priority": 5})
assert resp.status_code == 200
def test_archive_task(self, project_env):
"""归档不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/archive",
json={"agent": "agent1"})
assert resp.status_code == 200
class TestTaskCreateRoute:
"""POST /tasks 创建行为不变"""
def test_create_task(self, project_env):
"""创建格式不变"""
resp = client.post("/api/projects/test-proj/tasks",
json={"title": "New Task", "description": "test"})
assert resp.status_code == 201
data = resp.json()
assert "id" in data
```
---
## 4. expand 聚合测试(test_expand_api.py
```python
"""expand 聚合接口测试"""
import pytest
from fastapi.testclient import TestClient
from src.main import app
client = TestClient(app)
pytestmark = pytest.mark.integration
class TestExpandComments:
"""expand=comments"""
def test_comments_limit_and_count(self, expand_env):
"""返回最新 20 条 + total_count=25"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=comments")
data = resp.json()
comments = data["comments"]
assert isinstance(comments, dict)
assert len(comments["items"]) == 20
assert comments["total_count"] == 25
assert comments["limit"] == 20
def test_comments_are_latest(self, expand_env):
"""返回的是最新 20 条(Comment 5-24"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=comments")
data = resp.json()
first_content = data["comments"]["items"][0]["content"]
last_content = data["comments"]["items"][-1]["content"]
# 最新 20 条 = index 5 到 24
assert "5" in first_content or "24" in last_content
class TestExpandEvents:
"""expand=events"""
def test_events_limit_and_count(self, expand_env):
"""返回最新 30 条 + total_count=35"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=events")
data = resp.json()
events = data["events"]
assert isinstance(events, dict)
assert len(events["items"]) == 30
assert events["total_count"] == 35
assert events["limit"] == 30
class TestExpandFullResources:
"""outputs/reviews/decisions 全量返回"""
def test_expand_outputs_full(self, expand_env):
"""outputs 全量返回(5 条),格式是 list 不是 dict"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=outputs")
data = resp.json()
outputs = data["outputs"]
assert isinstance(outputs, list)
assert len(outputs) == 5
def test_expand_reviews_full(self, expand_env):
"""reviews 全量返回(3 条)"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=reviews")
data = resp.json()
reviews = data["reviews"]
assert isinstance(reviews, list)
assert len(reviews) == 3
def test_expand_decisions_full(self, expand_env):
"""decisions 全量返回(2 条)"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=decisions")
data = resp.json()
decisions = data["decisions"]
assert isinstance(decisions, list)
assert len(decisions) == 2
class TestExpandCombinations:
"""组合 expand"""
def test_expand_multiple_fields(self, expand_env):
"""expand=comments,outputs,reviews 组合"""
resp = client.get(
"/api/projects/test-proj/tasks/t1?expand=comments,outputs,reviews"
)
data = resp.json()
assert "comments" in data
assert "outputs" in data
assert "reviews" in data
assert "events" not in data # 未请求
assert "decisions" not in data
def test_expand_all_compat(self, expand_env):
"""expand=all 向后兼容"""
resp = client.get("/api/projects/test-proj/tasks/t1?expand=all")
data = resp.json()
# all 返回所有关联资源
assert "comments" in data
assert "outputs" in data
assert "reviews" in data
assert "events" in data
assert "decisions" in data
def test_no_expand(self, expand_env):
"""不传 expand 只返回基本 task"""
resp = client.get("/api/projects/test-proj/tasks/t1")
data = resp.json()
assert "comments" not in data
assert "outputs" not in data
assert data["id"] == "t1"
def test_expand_invalid_field_ignored(self, expand_env):
"""无效 expand 字段静默忽略"""
resp = client.get(
"/api/projects/test-proj/tasks/t1?expand=comments,invalid_field"
)
data = resp.json()
assert "comments" in data
assert "invalid_field" not in data
```
---
## 5. task_relation_routes.py 回归测试
```python
"""task_relation_routes.py 路由回归 — 验证拆分后行为不变"""
import pytest
from fastapi.testclient import TestClient
from src.main import app
client = TestClient(app)
pytestmark = pytest.mark.integration
class TestRelationRoutesRegression:
"""关联路由回归测试"""
def test_comments_crud(self, project_env):
"""GET/POST comments 不变"""
# POST
resp = client.post("/api/projects/test-proj/tasks/t1/comments",
json={"agent": "a1", "comment_type": "general",
"content": "test comment"})
assert resp.status_code == 201
# GET
resp = client.get("/api/projects/test-proj/tasks/t1/comments")
assert resp.status_code == 200
assert len(resp.json()["comments"]) >= 1
def test_outputs_crud(self, project_env):
"""GET/POST outputs 不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/outputs",
json={"agent": "a1", "type": "code",
"content": "print('hello')"})
assert resp.status_code == 201
resp = client.get("/api/projects/test-proj/tasks/t1/outputs")
assert resp.status_code == 200
def test_write_output_with_filename(self, project_env):
"""output 含 filename 的文件写入不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/outputs",
json={"agent": "a1", "type": "code",
"content": "x = 1",
"filename": "test.py"})
assert resp.status_code == 201
def test_write_output_invalid_type(self, project_env):
"""output 无效 type 返回 422"""
resp = client.post("/api/projects/test-proj/tasks/t1/outputs",
json={"agent": "a1", "type": "invalid_type",
"content": "x"})
assert resp.status_code == 422
def test_reviews_crud(self, project_env):
"""GET/POST reviews 不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/reviews",
json={"reviewer": "a1", "verdict": "APPROVE",
"confidence": 0.9, "risk_level": "low",
"summary": "LGTM"})
assert resp.status_code == 201
resp = client.get("/api/projects/test-proj/tasks/t1/reviews")
assert resp.status_code == 200
def test_decisions_crud(self, project_env):
"""GET/POST decisions 不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/decisions",
json={"agent": "a1", "decision_type": "scope",
"rationale": "test"})
assert resp.status_code == 201
resp = client.get("/api/projects/test-proj/tasks/t1/decisions")
assert resp.status_code == 200
def test_observations_add(self, project_env):
"""POST observations 不变"""
resp = client.post("/api/projects/test-proj/tasks/t1/observations",
json={"agent": "a1", "observation_type": "note",
"content": "observed"})
assert resp.status_code == 201
def test_events_list(self, project_env):
"""GET events 不变"""
resp = client.get("/api/projects/test-proj/tasks/t1/events")
assert resp.status_code == 200
assert "events" in resp.json()
def test_project_events(self, project_env):
"""GET /events 项目级事件不变"""
resp = client.get("/api/projects/test-proj/events")
assert resp.status_code == 200
def test_summary(self, project_env):
"""GET /summary 不变"""
resp = client.get("/api/projects/test-proj/summary")
assert resp.status_code == 200
```
---
## 6. CI 集成
### .gitea/workflows/ci.yml 新增步骤
```yaml
test:
runs-on: ubuntu-latest
steps:
# ... 现有步骤 ...
# API 兼容性验证
- name: Verify API Compatibility
run: |
cd src/frontend && npm run build
bash tests/scripts/verify_api_compat.sh
# 新增测试
- name: Run API Tests
run: |
pytest tests/unit/test_task_routes.py \
tests/unit/test_expand_api.py \
tests/integration/test_api.py \
-m "not e2e" -v
```
### 本地开发验证流程
```bash
# 1. 改完代码后先跑兼容性验证
bash tests/scripts/verify_api_compat.sh
# 2. 跑全量测试
pytest -m "not e2e" -v
# 3. 跑新增专项测试
pytest tests/unit/test_task_routes.py tests/unit/test_expand_api.py -v
```
---
## 7. 测试覆盖矩阵
| 设计文档章节 | 测试文件 | 测试类 | 用例数 |
|-------------|---------|--------|--------|
| §2 路由拆分(兼容性) | verify_api_compat.sh | — | 1 |
| §3.1 基本详情 | test_task_routes.py | TestTaskDetailRoutes | 2 |
| §3.2 搜索 | test_task_routes.py | TestTaskListRoutes | 5 |
| §3.3 动作路由 | test_task_routes.py | TestTaskActionRoutes | 5 |
| §3.4 创建 | test_task_routes.py | TestTaskCreateRoute | 1 |
| §4 expand comments | test_expand_api.py | TestExpandComments | 2 |
| §4 expand events | test_expand_api.py | TestExpandEvents | 1 |
| §4 expand 全量 | test_expand_api.py | TestExpandFullResources | 3 |
| §4 expand 组合 | test_expand_api.py | TestExpandCombinations | 4 |
| §5.2 关联回归 | test_api.py | TestRelationRoutesRegression | 10 |
| **合计** | | | **34** |