Files
2026Technology-Competition/tests/test_server_service.py
T
lhl 215c605650 feat(server): Web 服务化(FastAPI + SQLite + 内嵌零构建前端)
参赛成果物 03「交互界面 + 数据存储」落地:
- src/genesis/server/: store.py(SQLite 会话持久化)、service.py(会话化服务层:
  上传→解析→确认→影响→确认→生成→QA)、app.py(api-design §2 核心端点 9 组)、
  static/index.html(内嵌单页,零构建无 node_modules 依赖)
- scripts/serve.py 启动入口(--fake 离线引擎 / 默认真实 LLM)
- pyproject 加 fastapi/uvicorn/python-multipart
- 修复 qa_loop._build meta={} 导致真实模板 {{doc_title}} 等占位符残留 DocxInjectError
- README Web 服务说明 + service_url 登记指引;design.md §12.5 记录(含同步执行/
  zip 既有系统/无 WebSocket 的诚实偏差标注)
- 测试:test_server_store/service/api 共 34 用例(TestClient 全链路 + zip 影响流程 + 错误分支)
全量 pytest 473 passed / 99.20%
2026-08-26 22:13:19 +08:00

238 lines
8.3 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.
"""S3:会话化服务层测试(server/service.py)。
用真实样本文件 + FakeEngine 验证闭环:上传 → 解析 → 确认 → 影响 → 确认 → 生成 → QA。
"""
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import pytest
from genesis.server.store import SessionStore
from genesis.server.service import GenesisService, ServiceStepError, FileTypeError
_SAMPLE = Path(__file__).resolve().parents[1] / "sample"
class FakeEngine:
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
title = variables.get("title", "x")
# 日文样本(章节标题含假名)→ 返回日文正文,满足语言一致性强制
return SimpleNamespace(
data={
"title": title,
"blocks": [
{"type": "paragraph",
"text": "本機能はFakeLLMにより生成された十分な説明内容であり、書込規則を満たす。"},
],
},
status="ok",
)
@pytest.fixture
def svc(tmp_path):
return GenesisService(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
)
def _upload_core(svc, session_id):
svc.upload_file(session_id, "requirements", "requirements_newdev.xlsx",
(_SAMPLE / "requirements_newdev.xlsx").read_bytes())
svc.upload_file(session_id, "template", "template_design_ja.docx",
(_SAMPLE / "template_design_ja.docx").read_bytes())
svc.upload_file(session_id, "write_instruction", "rules_design_ja.docx",
(_SAMPLE / "rules_design_ja.docx").read_bytes())
svc.upload_file(session_id, "rules", "rules_entry_ja.docx",
(_SAMPLE / "rules_entry_ja.docx").read_bytes())
def test_create_and_upload_files(svc):
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
got = svc.get_session(s.session_id)
assert set(got.files.keys()) == {"requirements", "template", "write_instruction", "rules"}
assert got.files["requirements"]["name"] == "requirements_newdev.xlsx"
def test_upload_invalid_type_rejected(svc):
s = svc.create_session("u1")
with pytest.raises(FileTypeError):
svc.upload_file(s.session_id, "bogus", "x.txt", b"x")
def test_parse_requires_files(svc):
s = svc.create_session("u1")
with pytest.raises(ServiceStepError):
svc.run_parse(s.session_id)
def test_full_flow_fake(tmp_path):
"""完整闭环:解析 → 确认 → 影响 → 确认 → 生成 → QA → 结果路径就绪。"""
svc = GenesisService(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
)
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
svc.run_parse(s.session_id)
assert svc.get_session(s.session_id).status == "awaiting_parse_confirm"
assert "機能" in svc.get_session(s.session_id).structured_summary or "tables" in svc.get_session(s.session_id).structured_summary
svc.confirm_parse(s.session_id)
assert svc.get_session(s.session_id).status == "writing" # 无既有系统 → 影响跳过 → writing
svc.run_generate(s.session_id, output_language="auto")
got = svc.get_session(s.session_id)
assert got.status == "writing"
assert got.result_path and Path(got.result_path).is_file()
# 预览可读
html = svc.result_preview(s.session_id)
assert "<html" in html or "概要" in html or len(html) > 0
def test_generate_requires_confirmed_parse(svc):
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
with pytest.raises(ServiceStepError):
svc.run_generate(s.session_id) # 尚未解析确认
def test_run_qa_after_generate(tmp_path):
svc = GenesisService(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
)
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
svc.run_parse(s.session_id)
svc.confirm_parse(s.session_id)
svc.run_generate(s.session_id)
svc.run_qa(s.session_id)
got = svc.get_session(s.session_id)
assert got.qa_summary # QA 报告 JSON 已持久化
assert "language" in got.qa_summary or "overall_score" in got.qa_summary
def _make_existing_zip(tmp_path) -> bytes:
"""构造含 1 个 Java 文件的 zip(既有系统样本)。"""
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"
" @GetMapping public String list() { return \"ok\"; }\n}\n")
return buf.getvalue()
def test_existing_system_zip_triggers_impact_flow(tmp_path):
"""既有系统 zip 上传 → 影响调查全流程(run_impact/confirm_impact)。"""
svc = GenesisService(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
)
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
svc.upload_file(s.session_id, "existing_system", "existing.zip", _make_existing_zip(tmp_path))
svc.run_parse(s.session_id)
svc.confirm_parse(s.session_id)
assert svc.get_session(s.session_id).status == "impact_running"
svc.run_impact(s.session_id)
got = svc.get_session(s.session_id)
assert got.status == "awaiting_impact_confirm"
assert got.impact_summary
assert got.impact_report_path and Path(got.impact_report_path).exists()
svc.confirm_impact(s.session_id)
assert svc.get_session(s.session_id).status == "writing"
# 从 awaiting_impact_confirm 也能生成(兼容)
svc.run_generate(s.session_id)
assert svc.get_session(s.session_id).result_path
def test_impact_state_guard(svc):
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
svc.run_parse(s.session_id)
svc.confirm_parse(s.session_id)
# 无既有系统 → writingstart-impact 应报错
with pytest.raises(ServiceStepError):
svc.run_impact(s.session_id)
def test_qa_requires_generated(svc):
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
svc.run_parse(s.session_id)
svc.confirm_parse(s.session_id)
with pytest.raises(ServiceStepError):
svc.run_qa(s.session_id) # 尚未生成
def test_preview_missing_result_raises(svc):
s = svc.create_session("u1")
with pytest.raises(ServiceStepError):
svc.result_preview(s.session_id)
def test_preview_with_table(tmp_path):
"""预览含表格的 docx → 覆盖表格渲染分支(service 241-244)。"""
from docx import Document
from docx.table import Table
from genesis.server.store import SessionStore
svc = GenesisService(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
)
s = svc.create_session("u1")
doc_path = tmp_path / "data" / s.session_id / "with_table.docx"
doc_path.parent.mkdir(parents=True, exist_ok=True)
doc = Document()
doc.add_paragraph("概要段落")
t = doc.add_table(rows=1, cols=2)
t.rows[0].cells[0].text = "列A"
t.rows[0].cells[1].text = "列B"
doc.save(str(doc_path))
svc.store.update_session(s.session_id, result_path=str(doc_path))
html_out = svc.result_preview(s.session_id)
assert "<table" in html_out
assert "列A" in html_out
def test_confirm_parse_wrong_state_raises(svc):
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
# uploading 态直接确认解析 → 报错(service 139
with pytest.raises(ServiceStepError):
svc.confirm_parse(s.session_id)
def test_confirm_impact_wrong_state_raises(tmp_path):
svc = GenesisService(
store=SessionStore(db_path=str(tmp_path / "s.db")),
data_root=str(tmp_path / "data"),
engine=FakeEngine(),
)
s = svc.create_session("u1")
_upload_core(svc, s.session_id)
svc.run_parse(s.session_id)
svc.confirm_parse(s.session_id)
# writing 态直接确认影响 → 报错(service 167
with pytest.raises(ServiceStepError):
svc.confirm_impact(s.session_id)