# §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** |