Files
2026Technology-Competition/tests/test_server_service.py
T

337 lines
13 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 asyncio
import pytest
from genesis.server.store import SessionStore, ProjectsStore
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_create_session_name_and_project(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", name="我的会话", project="projA")
got = svc.get_session(s.session_id)
assert got.name == "我的会话"
assert got.project == "projA"
def test_upload_requirements_auto_names_session(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") # 默认「新会话」
svc.upload_file(s.session_id, "requirements", "要件定义_v2.xlsx", b"PK\x03\x04")
assert svc.get_session(s.session_id).name == "要件定义_v2"
def test_has_file_falls_back_to_project_config(tmp_path):
store = SessionStore(db_path=str(tmp_path / "s.db"))
projects = ProjectsStore(db_path=str(tmp_path / "p.db"))
projects.upsert(name="projA", display_name="P", template=str(_SAMPLE / "template_design_ja.docx"),
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
svc = GenesisService(
store=store, data_root=str(tmp_path / "data"), engine=FakeEngine(), projects=projects,
)
s = svc.create_session("u1", project="projA")
svc.upload_file(s.session_id, "requirements", "requirements_newdev.xlsx",
(_SAMPLE / "requirements_newdev.xlsx").read_bytes())
rec = svc.get_session(s.session_id)
# requirements 仅用户上传;template 来自项目配置
assert svc.has_file(rec, "requirements") is True
assert svc.has_file(rec, "template") is True
assert svc.has_file(rec, "write_instruction") is False
assert svc._eff_path(rec, "template") == str(_SAMPLE / "template_design_ja.docx")
def test_rebuild_source_merges_project_config(tmp_path):
store = SessionStore(db_path=str(tmp_path / "s.db"))
projects = ProjectsStore(db_path=str(tmp_path / "p.db"))
projects.upsert(name="projA", display_name="P", 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="")
svc = GenesisService(
store=store, data_root=str(tmp_path / "data"), engine=FakeEngine(), projects=projects,
)
s = svc.create_session("u1", project="projA")
svc.upload_file(s.session_id, "requirements", "requirements_newdev.xlsx",
(_SAMPLE / "requirements_newdev.xlsx").read_bytes())
ss = svc._rebuild_source(svc.get_session(s.session_id))
assert ss.template is not None
assert len(ss.rule_docs) >= 2 # write_instruction + rules 合并
def test_rebuild_source_enumerates_design_docs_dir(tmp_path):
from docx import Document
store = SessionStore(db_path=str(tmp_path / "s.db"))
projects = ProjectsStore(db_path=str(tmp_path / "p.db"))
dd = tmp_path / "design"
dd.mkdir()
d = Document()
d.add_paragraph("设计文档:订单管理整体方案")
d.save(dd / "d1.docx")
projects.upsert(name="projA", display_name="P", template="", write_instruction="",
rules=[], existing_system_code_dir="", design_docs_dir=str(dd))
svc = GenesisService(
store=store, data_root=str(tmp_path / "data"), engine=FakeEngine(), projects=projects,
)
s = svc.create_session("u1", project="projA")
svc.upload_file(s.session_id, "requirements", "requirements_newdev.xlsx",
(_SAMPLE / "requirements_newdev.xlsx").read_bytes())
ss = svc._rebuild_source(svc.get_session(s.session_id))
assert len(ss.design_docs) == 1
def test_has_file_without_projects_returns_false(svc):
s = svc.create_session("u1")
rec = svc.get_session(s.session_id)
assert svc.has_file(rec, "template") is False
assert svc._eff_path(rec, "template") is None
def test_upload_requirements_does_not_override_named_session(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", name="已命名会话")
svc.upload_file(s.session_id, "requirements", "other_name.xlsx", b"PK\x03\x04")
assert svc.get_session(s.session_id).name == "已命名会话"
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"
asyncio.run(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):
asyncio.run(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)