f55a037c98
- 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: 路由兼容性验证脚本
485 lines
16 KiB
Markdown
485 lines
16 KiB
Markdown
# §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** |
|