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%
This commit is contained in:
+136
-2
@@ -1,12 +1,15 @@
|
||||
"""S2:SQLite 会话存储测试(server/store.py)。
|
||||
|
||||
覆盖:创建会话 / 列表 / 状态更新 / 文件登记 / 结果路径 / 持久化重建。
|
||||
覆盖:创建会话 / 列表 / 状态更新 / 文件登记 / 结果路径 / 持久化重建 / 会话命名与项目绑定 / 项目配置存储。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from genesis.server.store import SessionStore, SessionRecord, SessionNotFoundError
|
||||
from genesis.server.store import (
|
||||
SessionStore, SessionRecord, SessionNotFoundError, ProjectsStore, ProjectConfigError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -79,3 +82,134 @@ def test_store_reload_persists(tmp_path):
|
||||
got = store2.get_session(s.session_id)
|
||||
assert got.status == "done"
|
||||
assert got.result_path == "x.docx"
|
||||
|
||||
|
||||
# ---------- 会话命名与项目绑定 ----------
|
||||
|
||||
def test_session_record_name_and_project_defaults():
|
||||
rec = SessionRecord(session_id="s1", user_id="u1")
|
||||
assert rec.name == "新会话"
|
||||
assert rec.project == ""
|
||||
assert rec.to_dict["name"] == "新会话"
|
||||
assert rec.to_dict["project"] == ""
|
||||
|
||||
|
||||
def test_create_session_with_name_and_project(store):
|
||||
s = store.create_session("u1", name="我的会话", project="projA")
|
||||
got = store.get_session(s.session_id)
|
||||
assert got.name == "我的会话"
|
||||
assert got.project == "projA"
|
||||
|
||||
|
||||
def test_session_persists_name_project(tmp_path):
|
||||
db = str(tmp_path / "s.db")
|
||||
s = SessionStore(db_path=db).create_session("u1", name="名字", project="p")
|
||||
reopened = SessionStore(db_path=db).get_session(s.session_id)
|
||||
assert reopened.name == "名字"
|
||||
assert reopened.project == "p"
|
||||
|
||||
|
||||
# ---------- 项目配置存储(ProjectsStore) ----------
|
||||
|
||||
@pytest.fixture
|
||||
def projects_store(tmp_path):
|
||||
return ProjectsStore(db_path=str(tmp_path / "projects.db"))
|
||||
|
||||
|
||||
def _make_docx(path: Path) -> str:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"PK\x03\x04") # 最小占位,仅用于存在性/后缀校验
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_projects_upsert_and_get(projects_store, tmp_path):
|
||||
tpl = _make_docx(tmp_path / "t.docx")
|
||||
wi = _make_docx(tmp_path / "wi.docx")
|
||||
rules_dir = tmp_path / "rules"
|
||||
rules_dir.mkdir()
|
||||
_make_docx(rules_dir / "r1.docx")
|
||||
cfg = projects_store.upsert(
|
||||
name="stock", display_name="股票系统", template=tpl, write_instruction=wi,
|
||||
rules=[str(rules_dir)], existing_system_code_dir="", design_docs_dir="",
|
||||
)
|
||||
assert cfg.name == "stock"
|
||||
got = projects_store.get("stock")
|
||||
assert got is not None
|
||||
assert got.display_name == "股票系统"
|
||||
assert got.template == tpl
|
||||
assert got.rules == [str(rules_dir / "r1.docx")]
|
||||
|
||||
|
||||
def test_projects_list_and_delete(projects_store, tmp_path):
|
||||
_make_docx(tmp_path / "t.docx")
|
||||
projects_store.upsert(name="p1", display_name="P1", template=str(tmp_path / "t.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
assert any(p.name == "p1" for p in projects_store.list())
|
||||
assert projects_store.delete("p1") is True
|
||||
assert projects_store.get("p1") is None
|
||||
|
||||
|
||||
def test_projects_upsert_enumerates_design_docs_dir(projects_store, tmp_path):
|
||||
_make_docx(tmp_path / "t.docx")
|
||||
dd = tmp_path / "design"
|
||||
dd.mkdir()
|
||||
_make_docx(dd / "d1.docx")
|
||||
_make_docx(dd / "d2.docx")
|
||||
cfg = projects_store.upsert(name="p", display_name="P", template=str(tmp_path / "t.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir="",
|
||||
design_docs_dir=str(dd))
|
||||
assert set(cfg.rules) == set() # 仅 design_docs_dir,rules 为空
|
||||
assert len(cfg.rules) == 0
|
||||
|
||||
|
||||
def test_projects_upsert_rejects_missing_template(projects_store, tmp_path):
|
||||
with pytest.raises(ProjectConfigError):
|
||||
projects_store.upsert(name="p", display_name="P", template=str(tmp_path / "nope.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
|
||||
|
||||
def test_projects_upsert_rejects_non_docx(projects_store, tmp_path):
|
||||
bad = tmp_path / "x.txt"
|
||||
bad.write_text("x")
|
||||
with pytest.raises(ProjectConfigError):
|
||||
projects_store.upsert(name="p", display_name="P", template=str(bad),
|
||||
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
|
||||
|
||||
def test_projects_upsert_rejects_missing_code_dir(projects_store, tmp_path):
|
||||
_make_docx(tmp_path / "t.docx")
|
||||
with pytest.raises(ProjectConfigError):
|
||||
projects_store.upsert(name="p", display_name="P", template=str(tmp_path / "t.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir=str(tmp_path / "missing"),
|
||||
design_docs_dir="")
|
||||
|
||||
|
||||
def test_projects_reupsert_updates(projects_store, tmp_path):
|
||||
_make_docx(tmp_path / "t.docx")
|
||||
projects_store.upsert(name="p", display_name="P", template=str(tmp_path / "t.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
projects_store.upsert(name="p", display_name="P2", template=str(tmp_path / "t.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
assert projects_store.get("p").display_name == "P2"
|
||||
|
||||
|
||||
def test_projects_code_dir_must_be_directory(projects_store, tmp_path):
|
||||
_make_docx(tmp_path / "t.docx")
|
||||
(tmp_path / "afile.txt").write_text("x")
|
||||
with pytest.raises(ProjectConfigError):
|
||||
projects_store.upsert(name="p", display_name="P", template=str(tmp_path / "t.docx"),
|
||||
write_instruction="", rules=[], existing_system_code_dir=str(tmp_path / "afile.txt"),
|
||||
design_docs_dir="")
|
||||
|
||||
|
||||
def test_projects_empty_template_skips_validation(projects_store, tmp_path):
|
||||
# 模板留空:不应触发文件校验(line 271/273 分支)
|
||||
cfg = projects_store.upsert(name="p", display_name="P", template="",
|
||||
write_instruction="", rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
assert cfg.template == ""
|
||||
|
||||
|
||||
def test_projects_upsert_empty_name_rejected(projects_store, tmp_path):
|
||||
with pytest.raises(ProjectConfigError):
|
||||
projects_store.upsert(name="", display_name="P", template="", write_instruction="",
|
||||
rules=[], existing_system_code_dir="", design_docs_dir="")
|
||||
|
||||
Reference in New Issue
Block a user