Files
2026Technology-Competition/tests/test_server_api.py
T
lhl 916c5beed7 feat(web): 项目级配置 + 会话命名/历史 + 设计文档纳入影响调查
- 会话支持 name/project 字段,上传要件定义后自动命名;前端侧边栏会话历史 + localStorage 恢复,顶部只显示会话名
- 新增 ProjectsStore(SQLite)与 /api/projects CRUD;绑定项目后 _rebuild_source 合并模板/规则/代码库/设计文档,上传区仅要件定义
- StructuredSource.design_docs 与 ImpactReport.design_references;影响调查新增既有设计文档确定性交叉引用(无 LLM)
- 同步更新 docs/design.md §12.7、README、_AI_USAGE_LOG.md;全量测试 558 通过,覆盖率 99.10%
2026-08-27 12:13:28 +08:00

350 lines
14 KiB
Python
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.
"""S4FastAPI 端点测试(server/app.pyhttpx TestClient 全链路)。"""
from __future__ import annotations
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from genesis.server.app import create_app
from genesis.server.store import SessionStore
_SAMPLE = Path(__file__).resolve().parents[1] / "sample"
@pytest.fixture
def client(tmp_path):
app = create_app(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine="fake",
)
return TestClient(app)
def _upload_core(client, sid):
files = [
("requirements", "requirements_newdev.xlsx", _SAMPLE / "requirements_newdev.xlsx"),
("template", "template_design_ja.docx", _SAMPLE / "template_design_ja.docx"),
("write_instruction", "rules_design_ja.docx", _SAMPLE / "rules_design_ja.docx"),
("rules", "rules_entry_ja.docx", _SAMPLE / "rules_entry_ja.docx"),
]
for ft, name, path in files:
r = client.post(f"/api/sessions/{sid}/files",
data={"file_type": ft},
files={"file": (name, path.read_bytes())})
assert r.status_code == 200, r.text
def test_health(client):
r = client.get("/api/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_create_and_get_session(client):
r = client.post("/api/sessions", json={"user_id": "u1"})
assert r.status_code == 200
sid = r.json()["session_id"]
g = client.get(f"/api/sessions/{sid}")
assert g.status_code == 200
assert g.json()["status"] == "uploading"
def test_session_list(client):
client.post("/api/sessions", json={"user_id": "u1"})
client.post("/api/sessions", json={"user_id": "u1"})
r = client.get("/api/sessions", params={"user_id": "u1"})
assert r.status_code == 200
assert len(r.json()) == 2
def test_full_flow_http(tmp_path):
"""HTTP 全链路:建会话 → 上传 → 解析 → 确认 → 生成 → QA → 预览/下载。"""
app = create_app(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine="fake",
)
client = TestClient(app)
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
r = client.post(f"/api/sessions/{sid}/start-parse")
assert r.status_code == 200
assert client.get(f"/api/sessions/{sid}").json()["status"] == "awaiting_parse_confirm"
pr = client.get(f"/api/sessions/{sid}/parse-result")
assert pr.status_code == 200
assert "tables" in pr.json()
r = client.post(f"/api/sessions/{sid}/confirm-parse")
assert r.status_code == 200
assert client.get(f"/api/sessions/{sid}").json()["status"] == "writing" # 无既有系统 → 影响跳过
r = client.post(f"/api/sessions/{sid}/generate", json={"output_language": "auto"})
assert r.status_code == 200
assert client.get(f"/api/sessions/{sid}").json()["result_path"]
r = client.post(f"/api/sessions/{sid}/run-qa")
assert r.status_code == 200
got = client.get(f"/api/sessions/{sid}").json()
assert got["status"] == "done"
assert "overall_score" in got["qa_summary"]
prev = client.get(f"/api/sessions/{sid}/result/preview")
assert prev.status_code == 200
assert "html" in prev.json()
dl = client.get(f"/api/sessions/{sid}/result/download")
assert dl.status_code == 200
assert dl.headers["content-type"].startswith("application/vnd.openxmlformats")
def test_state_transition_invalid_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
# uploading 直接 generate → 409
r = client.post(f"/api/sessions/{sid}/generate", json={})
assert r.status_code == 409
def test_upload_invalid_type_400(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
r = client.post(f"/api/sessions/{sid}/files",
data={"file_type": "bogus"},
files={"file": ("x.txt", b"x")})
assert r.status_code == 400
def test_root_serves_frontend(client):
r = client.get("/")
assert r.status_code == 200
assert "Genesis" in r.text
def test_delete_session(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
r = client.delete(f"/api/sessions/{sid}")
assert r.status_code == 200
assert r.json()["deleted"] is True
assert client.get(f"/api/sessions/{sid}").status_code == 404
def test_generate_requires_confirm_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
# 未确认解析就生成 → 409
r = client.post(f"/api/sessions/{sid}/generate", json={})
assert r.status_code == 409
def test_start_impact_without_existing_system_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/sessions/{sid}/start-parse")
client.post(f"/api/sessions/{sid}/confirm-parse")
# 无既有系统 → writingstart-impact 409
r = client.post(f"/api/sessions/{sid}/start-impact")
assert r.status_code == 409
def test_qa_requires_generate_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/sessions/{sid}/start-parse")
client.post(f"/api/sessions/{sid}/confirm-parse")
r = client.post(f"/api/sessions/{sid}/run-qa")
assert r.status_code == 409
def test_report_downloads_after_full_flow(tmp_path):
"""QA/影响报告下载端点(FileResponse 分支)。"""
app = create_app(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine="fake",
)
client = TestClient(app)
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/sessions/{sid}/start-parse")
client.post(f"/api/sessions/{sid}/confirm-parse")
client.post(f"/api/sessions/{sid}/generate", json={})
client.post(f"/api/sessions/{sid}/run-qa")
qa = client.get(f"/api/sessions/{sid}/result/qa-report")
assert qa.status_code == 200
assert "application/json" in qa.headers["content-type"]
qa_body = client.get(f"/api/sessions/{sid}/qa-result")
assert qa_body.status_code == 200
assert "overall_score" in qa_body.json()
# 无影响报告(未跑影响)→ 404
assert client.get(f"/api/sessions/{sid}/result/impact-report").status_code == 404
def test_session_not_found_404(client):
assert client.get("/api/sessions/nope").status_code == 404
def test_upload_to_missing_session_404(client):
r = client.post("/api/sessions/nope/files",
data={"file_type": "requirements"},
files={"file": ("a.xlsx", b"x")})
assert r.status_code == 404
def test_start_parse_no_files_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
assert client.post(f"/api/sessions/{sid}/start-parse").status_code == 409
def test_start_parse_missing_session_404(client):
assert client.post("/api/sessions/nope/start-parse").status_code == 404
def test_confirm_parse_wrong_state_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
# uploading 态直接确认解析 → 409
assert client.post(f"/api/sessions/{sid}/confirm-parse").status_code == 409
def _upload_zip(client, sid):
import io
import zipfile
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("demo/OrderController.java",
"package demo;\n@RestController public class OrderController {}\n")
client.post(f"/api/sessions/{sid}/files",
data={"file_type": "existing_system"},
files={"file": ("existing.zip", buf.getvalue())})
def test_impact_flow_http_zip(tmp_path):
"""HTTP 影响调查全流程 + 报告下载(app 164/168-170/174-178/226)。"""
app = create_app(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine="fake",
)
client = TestClient(app)
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
_upload_zip(client, sid)
client.post(f"/api/sessions/{sid}/start-parse")
assert client.post(f"/api/sessions/{sid}/confirm-parse").json()["status"] == "impact_running"
r = client.post(f"/api/sessions/{sid}/start-impact")
assert r.status_code == 200
ir = client.get(f"/api/sessions/{sid}/impact-result")
assert ir.status_code == 200 and "summary" in ir.json()
assert client.post(f"/api/sessions/{sid}/confirm-impact").json()["status"] == "writing"
dl = client.get(f"/api/sessions/{sid}/result/impact-report")
assert dl.status_code == 200 and "application/json" in dl.headers["content-type"]
def test_confirm_impact_wrong_state_409(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/sessions/{sid}/start-parse")
client.post(f"/api/sessions/{sid}/confirm-parse")
# writing(无既有系统)直接确认影响 → 409
assert client.post(f"/api/sessions/{sid}/confirm-impact").status_code == 409
def test_download_result_not_found_404(client):
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
assert client.get(f"/api/sessions/{sid}/result/download").status_code == 404
assert client.get(f"/api/sessions/{sid}/result/qa-report").status_code == 404
def test_real_mode_generate_without_key_500(tmp_path, monkeypatch):
"""engine=None(真实模式)→ generate 按需 buildbuild 抛错 → 500。"""
import genesis.server.app as app_mod
def boom():
raise RuntimeError("LLMNotConfiguredError: 缺少 API Key")
monkeypatch.setattr(app_mod, "build_inference_engine", boom)
app = create_app(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=None,
)
client = TestClient(app, raise_server_exceptions=False)
sid = client.post("/api/sessions", json={"user_id": "u1"}).json()["session_id"]
_upload_core(client, sid)
client.post(f"/api/sessions/{sid}/start-parse")
client.post(f"/api/sessions/{sid}/confirm-parse")
r = client.post(f"/api/sessions/{sid}/generate", json={})
assert r.status_code == 500
# ---------- 项目配置(S6 ----------
def test_create_and_list_projects(client):
body = {
"name": "stock", "display_name": "股票系统",
"template": str(_SAMPLE / "template_design_ja.docx"),
"write_instruction": "", "rules": [],
"existing_system_code_dir": "", "design_docs_dir": "",
}
r = client.post("/api/projects", json=body)
assert r.status_code == 200
assert r.json()["name"] == "stock"
lst = client.get("/api/projects").json()
assert any(p["name"] == "stock" for p in lst)
one = client.get("/api/projects/stock").json()
assert one["template"] == str(_SAMPLE / "template_design_ja.docx")
def test_project_invalid_template_400(client):
r = client.post("/api/projects", json={"name": "p", "template": "/no/such.docx"})
assert r.status_code == 400
assert r.json()["detail"]["code"] == "PROJECT_CONFIG_INVALID"
def test_delete_project(client):
client.post("/api/projects", json={"name": "p", "template": str(_SAMPLE / "template_design_ja.docx")})
assert client.delete("/api/projects/p").json()["deleted"] is True
assert client.get("/api/projects/p").status_code == 404
def test_session_accepts_name_and_project(client):
r = client.post("/api/sessions", json={"user_id": "u1", "name": "我的会话", "project": "projA"})
assert r.status_code == 200
sid = r.json()["session_id"]
assert r.json()["name"] == "我的会话"
g = client.get(f"/api/sessions/{sid}").json()
assert g["name"] == "我的会话"
assert g["project"] == "projA"
def test_session_list_includes_name_project(client):
client.post("/api/sessions", json={"user_id": "u1", "name": "n1", "project": "pA"})
r = client.get("/api/sessions", params={"user_id": "u1"}).json()
assert any(s["name"] == "n1" and s["project"] == "pA" for s in r)
def test_generate_with_project_config_no_template_upload(client):
"""绑定项目(含模板/规则)后,仅上传要件定义即可解析生成。"""
client.post("/api/projects", json={
"name": "projA", "template": str(_SAMPLE / "template_design_ja.docx"),
"write_instruction": str(_SAMPLE / "rules_design_ja.docx"),
"rules": [str(_SAMPLE / "rules_entry_ja.docx")],
"existing_system_code_dir": "", "design_docs_dir": "",
})
sid = client.post("/api/sessions", json={"user_id": "u1", "project": "projA"}).json()["session_id"]
# 仅上传要件定义
r = client.post(f"/api/sessions/{sid}/files", data={"file_type": "requirements"},
files={"file": ("requirements_newdev.xlsx", (_SAMPLE / "requirements_newdev.xlsx").read_bytes())})
assert r.status_code == 200
p = client.post(f"/api/sessions/{sid}/start-parse")
assert p.status_code == 200, p.text
assert client.post(f"/api/sessions/{sid}/confirm-parse").status_code == 200
# 无既有系统 → 直接 writing,可生成
gen = client.post(f"/api/sessions/{sid}/generate", json={})
assert gen.status_code == 200, gen.text
assert client.get(f"/api/sessions/{sid}/result/download").status_code == 200