AgentEvalTool/tests/integration/test_files_api.py
sinohqb 3705945a7d test: 完整测试覆盖补全(+163 用例)
架构重构(候选 1-6):
- storage/repository.py 按域拆分为包(target/scenario/run/campaign/result)
- storage/db.py 按域拆分为包(eval/campaign/file/model_config/intelligent_eval)
- intelligent_eval/lifecycle.py 按状态机阶段拆分为包
- services/runs.py 编排逻辑下沉
- Campaigns.tsx 拆分为 campaigns/ 子组件

测试补全(候选 7):
前端(+125 用例,107→232):
- utils/ 纯函数:date/campaignTime/ruleLabels/fileTree/fileFormat/colors
- stores/tabStore 状态管理
- 核心组件:FormDrawer/PageWrapper/ChatBubble/GeneratedMessages/SectionHeader/StatCard/TurnList
- 业务组件:CaseBlock/CaseDetail/RuleOverview/WindowTimeline/RunList/TabBar/CampaignRunTimeline
- 文件管理:FileCategoryTree/FileTable
- hooks:sessionReducer/useFiles/useRunSession

后端(+38 用例,916→954):
- targets API CRUD + 404 路径
- WebSocket 连接管理器
- proxy 头部重写(CSP/X-Frame-Options)
- target 仓储 update 方法
- app 健康检查 + SPA 404
- scenarios 模板端点 + 404
- files API 边缘分支(404 场景 + 500 兜底)
- files service update_category
- 智能评估状态机迁移测试

门禁状态:
- 前端:tsc 干净 + 232 passed
- 后端:954 passed + ruff 全绿
2026-08-24 15:56:09 +08:00

309 lines
9.9 KiB
Python

"""Integration tests for the file management API."""
from pathlib import Path
import pytest
from agenteval.config import get_settings
from agenteval.web.app import app
from agenteval.web.deps import get_db
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine
@pytest.fixture()
def files_client(tmp_path: Path, monkeypatch):
from agenteval.storage.db import ( # noqa: F401
EvalResultDB,
EvalRunDB,
EvalTargetDB,
FileCategoryDB,
FileRecordDB,
ScenarioDB,
TurnDB,
)
from agenteval.web.routers import files as files_module
engine = create_engine(
f"sqlite:///{tmp_path / 'files_api.db'}",
connect_args={"check_same_thread": False},
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
files_dir = tmp_path / "files"
files_dir.mkdir()
monkeypatch.setattr(files_module, "FILES_DIR", files_dir)
settings = get_settings()
monkeypatch.setattr(settings, "allowed_extensions", "txt,json")
monkeypatch.setattr(settings, "max_upload_size_mb", 1)
def override_get_db():
yield session
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client, session, files_dir
app.dependency_overrides.clear()
session.close()
engine.dispose()
def _create_category(client: TestClient, name: str, parent_id: str | None = None) -> dict:
response = client.post(
"/api/files/categories",
json={"name": name, "parent_id": parent_id},
)
assert response.status_code == 200
return response.json()
def test_upload_config_uses_server_settings(files_client):
client, _, _ = files_client
response = client.get("/api/files/config")
assert response.status_code == 200
assert response.json() == {
"allowed_extensions": ["json", "txt"],
"max_upload_size_mb": 1,
}
def test_category_crud_returns_domain_fields_and_deep_tree(files_client):
client, _, _ = files_client
root = _create_category(client, "根分类")
child = _create_category(client, "二级", root["id"])
leaf = _create_category(client, "三级", child["id"])
tree = client.get("/api/files/categories").json()
assert tree[0]["id"] == root["id"]
assert tree[0]["name"] == "根分类"
assert tree[0]["children"][0]["id"] == child["id"]
assert tree[0]["children"][0]["children"][0]["id"] == leaf["id"]
updated = client.put(
f"/api/files/categories/{leaf['id']}",
json={"name": "三级分类"},
)
assert updated.status_code == 200
assert updated.json()["name"] == "三级分类"
def test_create_category_rejects_missing_parent(files_client):
client, _, _ = files_client
response = client.post(
"/api/files/categories",
json={"name": "孤立分类", "parent_id": "missing"},
)
assert response.status_code == 400
assert response.json()["detail"] == "父分类不存在"
def test_list_files_includes_all_descendant_categories(files_client):
client, _, _ = files_client
root = _create_category(client, "根分类")
child = _create_category(client, "二级", root["id"])
leaf = _create_category(client, "三级", child["id"])
upload = client.post(
"/api/files/upload",
data={"category_id": leaf["id"]},
files={"file": ("deep.txt", b"deep content", "text/plain")},
)
assert upload.status_code == 200
response = client.get("/api/files", params={"category_id": root["id"]})
assert response.status_code == 200
assert [item["original_name"] for item in response.json()] == ["deep.txt"]
def test_upload_download_and_delete_file(files_client):
client, _, files_dir = files_client
uploaded = client.post(
"/api/files/upload",
files={"file": ("notes.txt", b"hello", "text/plain")},
)
assert uploaded.status_code == 200
record = uploaded.json()
assert record["original_name"] == "notes.txt"
assert record["file_size"] == 5
assert record["storage_directory"] == "data/files"
assert client.get("/api/files").json()[0]["storage_directory"] == "data/files"
assert len(list(files_dir.iterdir())) == 1
downloaded = client.get(f"/api/files/{record['id']}/download")
assert downloaded.status_code == 200
assert downloaded.content == b"hello"
deleted = client.delete(f"/api/files/{record['id']}")
assert deleted.status_code == 200
assert list(files_dir.iterdir()) == []
assert client.get("/api/files").json() == []
def test_upload_rejects_invalid_category_and_extension(files_client):
client, _, files_dir = files_client
invalid_category = client.post(
"/api/files/upload",
data={"category_id": "missing"},
files={"file": ("notes.txt", b"hello", "text/plain")},
)
invalid_extension = client.post(
"/api/files/upload",
files={"file": ("notes.exe", b"hello", "application/octet-stream")},
)
assert invalid_category.status_code == 400
assert invalid_category.json()["detail"] == "分类不存在"
assert invalid_extension.status_code == 400
assert list(files_dir.iterdir()) == []
def test_upload_rejects_oversized_file_without_disk_artifact(files_client):
client, _, files_dir = files_client
response = client.post(
"/api/files/upload",
files={"file": ("large.txt", b"x" * (1024 * 1024 + 1), "text/plain")},
)
assert response.status_code == 413
assert list(files_dir.iterdir()) == []
assert client.get("/api/files").json() == []
def test_delete_category_removes_descendant_files(files_client):
client, _, files_dir = files_client
root = _create_category(client, "根分类")
child = _create_category(client, "子分类", root["id"])
uploaded = client.post(
"/api/files/upload",
data={"category_id": child["id"]},
files={"file": ("child.txt", b"content", "text/plain")},
)
assert uploaded.status_code == 200
assert len(list(files_dir.iterdir())) == 1
response = client.delete(f"/api/files/categories/{root['id']}")
assert response.status_code == 200
assert client.get("/api/files/categories").json() == []
assert client.get("/api/files").json() == []
assert list(files_dir.iterdir()) == []
# ── Edge-case error branches ──────────────────────────────────────────────────
def test_update_nonexistent_category_returns_404(files_client):
"""update_category catches CategoryNotFoundError → 404."""
client, _, _ = files_client
response = client.put(
"/api/files/categories/nonexistent",
json={"name": "新名称"},
)
assert response.status_code == 404
assert response.json()["detail"] == "分类不存在"
def test_delete_nonexistent_category_returns_404(files_client):
"""delete_category catches CategoryNotFoundError → 404."""
client, _, _ = files_client
response = client.delete("/api/files/categories/nonexistent")
assert response.status_code == 404
assert response.json()["detail"] == "分类不存在"
def test_list_files_with_invalid_category_returns_404(files_client):
"""list_files catches CategoryNotFoundError → 404."""
client, _, _ = files_client
response = client.get("/api/files", params={"category_id": "nonexistent"})
assert response.status_code == 404
assert response.json()["detail"] == "分类不存在"
def test_download_nonexistent_file_returns_404(files_client):
"""download_file catches FileRecordNotFoundError → 404."""
client, _, _ = files_client
response = client.get("/api/files/nonexistent/download")
assert response.status_code == 404
assert response.json()["detail"] == "文件不存在"
def test_delete_nonexistent_file_returns_404(files_client):
"""delete_file catches FileRecordNotFoundError → 404."""
client, _, _ = files_client
response = client.delete("/api/files/nonexistent")
assert response.status_code == 404
assert response.json()["detail"] == "文件不存在"
def test_download_file_with_missing_physical_file_returns_404(files_client, monkeypatch):
"""download_file catches PhysicalFileNotFoundError → 404 when DB record exists but physical file is missing."""
client, session, files_dir = files_client
from agenteval.storage.db import FileRecordDB
# Upload a file first
uploaded = client.post(
"/api/files/upload",
files={"file": ("notes.txt", b"hello", "text/plain")},
)
assert uploaded.status_code == 200
record = uploaded.json()
file_id = record["id"]
# Get storage_name from DB (not exposed in API response)
db_record = session.get(FileRecordDB, file_id)
assert db_record is not None
physical_path = files_dir / db_record.storage_name
# Delete the physical file but keep the DB record
physical_path.unlink()
# Now try to download — should get 404 "物理文件不存在"
response = client.get(f"/api/files/{file_id}/download")
assert response.status_code == 404
assert response.json()["detail"] == "物理文件不存在"
def test_unknown_file_management_error_returns_500(files_client, monkeypatch):
"""_raise_http_error fallback: unknown FileManagementError subclass → 500."""
client, _, _ = files_client
from agenteval.services.files import FileManagementError, FileManagementService
class UnknownError(FileManagementError):
pass
def raise_unknown(self, category_id, name):
raise UnknownError("未知错误")
# Use update_category which has try/except FileManagementError
monkeypatch.setattr(FileManagementService, "update_category", raise_unknown)
response = client.put(
"/api/files/categories/some-id",
json={"name": "新名称"},
)
assert response.status_code == 500
assert response.json()["detail"] == "文件操作失败"