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:
lhl
2026-08-27 12:13:28 +08:00
parent 838a93720e
commit 916c5beed7
18 changed files with 1174 additions and 65 deletions
+136 -2
View File
@@ -1,12 +1,15 @@
"""S2SQLite 会话存储测试(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_dirrules 为空
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="")