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%
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
"""S4:FastAPI 端点测试(server/app.py,httpx 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")
|
||||
# 无既有系统 → writing,start-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 按需 build;build 抛错 → 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
|
||||
@@ -0,0 +1,237 @@
|
||||
"""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)
|
||||
# 无既有系统 → writing,start-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)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""S2:SQLite 会话存储测试(server/store.py)。
|
||||
|
||||
覆盖:创建会话 / 列表 / 状态更新 / 文件登记 / 结果路径 / 持久化重建。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from genesis.server.store import SessionStore, SessionRecord, SessionNotFoundError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path):
|
||||
return SessionStore(db_path=str(tmp_path / "sessions.db"))
|
||||
|
||||
|
||||
def test_create_session(store):
|
||||
s = store.create_session(user_id="u1")
|
||||
assert s.session_id
|
||||
assert s.user_id == "u1"
|
||||
assert s.status == "uploading"
|
||||
assert s.files == {}
|
||||
assert s.created_at
|
||||
|
||||
|
||||
def test_get_session(store):
|
||||
s = store.create_session("u1")
|
||||
got = store.get_session(s.session_id)
|
||||
assert got.session_id == s.session_id
|
||||
assert got.status == "uploading"
|
||||
|
||||
|
||||
def test_get_missing_session_raises(store):
|
||||
with pytest.raises(SessionNotFoundError):
|
||||
store.get_session("nope")
|
||||
|
||||
|
||||
def test_list_sessions_by_user(store):
|
||||
a = store.create_session("u1")
|
||||
b = store.create_session("u1")
|
||||
store.create_session("u2")
|
||||
lst = store.list_sessions("u1")
|
||||
ids = {s.session_id for s in lst}
|
||||
assert ids == {a.session_id, b.session_id}
|
||||
|
||||
|
||||
def test_update_status(store):
|
||||
s = store.create_session("u1")
|
||||
store.update_status(s.session_id, "parsing")
|
||||
assert store.get_session(s.session_id).status == "parsing"
|
||||
|
||||
|
||||
def test_update_fields_merge(store):
|
||||
s = store.create_session("u1")
|
||||
store.update_session(s.session_id, files={"requirements": {"file_id": "f1", "name": "a.xlsx", "size": 10}})
|
||||
got = store.get_session(s.session_id)
|
||||
assert got.files["requirements"]["file_id"] == "f1"
|
||||
# 保留既有字段
|
||||
assert got.status == "uploading"
|
||||
|
||||
|
||||
def test_set_result_paths(store):
|
||||
s = store.create_session("u1")
|
||||
store.update_session(s.session_id, result_path="out.docx", impact_report_path="ir.json", qa_report_path="qa.json")
|
||||
got = store.get_session(s.session_id)
|
||||
assert got.result_path == "out.docx"
|
||||
assert got.impact_report_path == "ir.json"
|
||||
assert got.qa_report_path == "qa.json"
|
||||
|
||||
|
||||
def test_store_reload_persists(tmp_path):
|
||||
db = str(tmp_path / "s.db")
|
||||
store1 = SessionStore(db_path=db)
|
||||
s = store1.create_session("u1")
|
||||
store1.update_status(s.session_id, "done")
|
||||
store1.update_session(s.session_id, result_path="x.docx")
|
||||
# 重新打开同一 db → 数据仍在
|
||||
store2 = SessionStore(db_path=db)
|
||||
got = store2.get_session(s.session_id)
|
||||
assert got.status == "done"
|
||||
assert got.result_path == "x.docx"
|
||||
Reference in New Issue
Block a user