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:
@@ -95,6 +95,87 @@ def test_status_reports_current_state(tmp_path):
|
||||
assert "uploading" in r["reply"]
|
||||
|
||||
|
||||
def test_status_reply_with_project_design_docs(tmp_path):
|
||||
from genesis.server.store import ProjectsStore
|
||||
store = SessionStore(db_path=str(tmp_path / "s.db"))
|
||||
dd = tmp_path / "design"
|
||||
dd.mkdir()
|
||||
(dd / "d.docx").write_bytes(b"PK\x03\x04")
|
||||
projects = ProjectsStore(db_path=str(tmp_path / "p.db"))
|
||||
projects.upsert(name="projA", display_name="P", template="", write_instruction="",
|
||||
rules=[], existing_system_code_dir="", design_docs_dir=str(dd))
|
||||
agent = ChatAgent(GenesisService(store=store, data_root=str(tmp_path / "data"),
|
||||
engine=FakeEngine(), projects=projects), fake=True)
|
||||
sid = agent.service.create_session("u1", project="projA").session_id
|
||||
r = agent.handle_message(sid, "现在什么状态?")
|
||||
assert "design_docs" in r["reply"]
|
||||
|
||||
|
||||
def test_status_reply_lists_existing_system_and_design_docs(tmp_path):
|
||||
from genesis.server.store import ProjectsStore
|
||||
store = SessionStore(db_path=str(tmp_path / "s.db"))
|
||||
dd = tmp_path / "design"; dd.mkdir(); (dd / "d.docx").write_bytes(b"PK\x03\x04")
|
||||
code = tmp_path / "code"; code.mkdir()
|
||||
projects = ProjectsStore(db_path=str(tmp_path / "p.db"))
|
||||
projects.upsert(name="projA", display_name="P", template="", write_instruction="",
|
||||
rules=[], existing_system_code_dir=str(code), design_docs_dir=str(dd))
|
||||
agent = ChatAgent(GenesisService(store=store, data_root=str(tmp_path / "data"),
|
||||
engine=FakeEngine(), projects=projects), fake=True)
|
||||
sid = agent.service.create_session("u1", project="projA").session_id
|
||||
reply = agent._status_reply(agent.service.get_session(sid), [])
|
||||
assert "existing_system" in reply["reply"] and "design_docs" in reply["reply"]
|
||||
|
||||
|
||||
def test_auto_generate_unknown_status_falls_to_status_reply(tmp_path):
|
||||
from genesis.server.store import ProjectsStore
|
||||
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="")
|
||||
agent = ChatAgent(GenesisService(store=store, data_root=str(tmp_path / "data"),
|
||||
engine=FakeEngine(), projects=projects), fake=True)
|
||||
sid = agent.service.create_session("u1", project="projA").session_id
|
||||
# 上传要件定义,使 has_file 通过;随后将状态置为 done 触发 _status_reply 分支
|
||||
agent.service.upload_file(sid, "requirements", "requirements_newdev.xlsx",
|
||||
(_SAMPLE / "requirements_newdev.xlsx").read_bytes())
|
||||
agent.service.store.update_status(sid, "done")
|
||||
r = agent._auto_generate(sid, "auto", [])
|
||||
assert "done" in r["reply"]
|
||||
|
||||
|
||||
def test_run_generate_from_awaiting_impact_confirm_confirms_first(tmp_path):
|
||||
agent = ChatAgent(_svc(tmp_path), fake=True)
|
||||
sid = agent.service.create_session("u1").session_id
|
||||
agent.service.store.update_status(sid, "awaiting_impact_confirm")
|
||||
rec = agent.service.get_session(sid)
|
||||
r = agent._run_generate(sid, rec, [], output_language="auto")
|
||||
# confirm_impact 先执行(line 194),随后 generate 因缺文件抛错被捕获
|
||||
assert r["status"] in ("writing", "awaiting_impact_confirm")
|
||||
|
||||
|
||||
def test_run_impact_from_awaiting_parse_confirm(tmp_path):
|
||||
from genesis.server.store import ProjectsStore
|
||||
store = SessionStore(db_path=str(tmp_path / "s.db"))
|
||||
code = tmp_path / "code"; code.mkdir(); (code / "A.java").write_text("class A {}")
|
||||
projects = ProjectsStore(db_path=str(tmp_path / "p.db"))
|
||||
projects.upsert(name="projA", display_name="P", template="", write_instruction="",
|
||||
rules=[], existing_system_code_dir=str(code), design_docs_dir="")
|
||||
agent = ChatAgent(GenesisService(store=store, data_root=str(tmp_path / "data"),
|
||||
engine=FakeEngine(), projects=projects), fake=True)
|
||||
sid = agent.service.create_session("u1", project="projA").session_id
|
||||
agent.service.store.update_status(sid, "awaiting_parse_confirm")
|
||||
r = agent._run_impact(sid, [])
|
||||
assert r["status"] == "awaiting_impact_confirm"
|
||||
|
||||
|
||||
def test_impact_brief_malformed_returns_default(tmp_path):
|
||||
agent = ChatAgent(_svc(tmp_path), fake=True)
|
||||
rec = agent.service.create_session("u1")
|
||||
rec.impact_summary = "not-json"
|
||||
assert agent._impact_brief(rec) == "已完成"
|
||||
|
||||
|
||||
def test_generate_without_files_hints_upload(tmp_path):
|
||||
agent = ChatAgent(_svc(tmp_path), fake=True)
|
||||
sid = agent.service.create_session("u1").session_id
|
||||
|
||||
@@ -3,9 +3,9 @@ import pytest
|
||||
|
||||
from genesis.data_models import (
|
||||
CellValue, ChangeType, ControllerInfo, EntityInfo, ExcelTable,
|
||||
ExistingSystemInfo, Provenance, ServiceInfo, SheetType, StructuredSource,
|
||||
ExistingSystemInfo, Provenance, RuleDocument, ServiceInfo, SheetType, StructuredSource,
|
||||
)
|
||||
from genesis.impact.impact_agent import ImpactAgent
|
||||
from genesis.impact.impact_agent import ImpactAgent, impact_report_to_dict
|
||||
|
||||
|
||||
def _cv(value) -> CellValue:
|
||||
@@ -201,3 +201,40 @@ def test_impact_report_to_dict_serializable():
|
||||
assert d["change_analysis"]["new_elements"][0]["change_type"] == "新規"
|
||||
assert d["change_analysis"]["modified_elements"][0]["impacted_existing"] == ["OrderController"]
|
||||
assert d["summary"]["modified"] == 1
|
||||
|
||||
|
||||
def _doc(name: str, content: str) -> RuleDocument:
|
||||
return RuleDocument(file_name=name, category="design", markdown_content=content,
|
||||
source_path=name, file_type="word", hash="h")
|
||||
|
||||
|
||||
def _ss_with_design(existing, tables, docs) -> StructuredSource:
|
||||
return StructuredSource(tables=tables, template=None, rule_docs=[], image_analyses=[],
|
||||
existing_system=existing, comments=[], design_docs=docs)
|
||||
|
||||
|
||||
def test_design_doc_cross_reference_hits_identifier():
|
||||
docs = [_doc("design_a.docx", "本章描述 OrderController 的限价单处理逻辑与风控。")]
|
||||
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F001", "新功能", "新規", "")])]
|
||||
report = ImpactAgent().run(_ss_with_design(_existing(), tables, docs), session_id="d")
|
||||
refs = report.design_references
|
||||
ids = {r.identifier for r in refs}
|
||||
assert "OrderController" in ids
|
||||
assert refs[0].doc_name == "design_a.docx"
|
||||
assert "OrderController" in refs[0].snippet
|
||||
# 序列化也包含
|
||||
assert any(r["identifier"] == "OrderController"
|
||||
for r in impact_report_to_dict(report)["design_references"])
|
||||
|
||||
|
||||
def test_design_doc_cross_reference_empty_when_no_docs():
|
||||
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F001", "新功能", "新規", "")])]
|
||||
report = ImpactAgent().run(_ss_with_design(_existing(), tables, []), session_id="d")
|
||||
assert report.design_references == []
|
||||
|
||||
|
||||
def test_design_doc_with_empty_content_skipped():
|
||||
docs = [_doc("empty.docx", "")] # 无正文 → continue(line 164)
|
||||
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F001", "新功能", "新規", "")])]
|
||||
report = ImpactAgent().run(_ss_with_design(_existing(), tables, docs), session_id="d")
|
||||
assert report.design_references == [] # 无正文设计文档被跳过,line 165 正常执行
|
||||
|
||||
@@ -279,3 +279,71 @@ def test_real_mode_generate_without_key_500(tmp_path, monkeypatch):
|
||||
client.post(f"/api/sessions/{sid}/confirm-parse")
|
||||
r = client.post(f"/api/sessions/{sid}/generate", json={})
|
||||
assert r.status_code == 500
|
||||
|
||||
|
||||
# ---------- 项目配置(S6) ----------
|
||||
|
||||
def test_create_and_list_projects(client):
|
||||
body = {
|
||||
"name": "stock", "display_name": "股票系统",
|
||||
"template": str(_SAMPLE / "template_design_ja.docx"),
|
||||
"write_instruction": "", "rules": [],
|
||||
"existing_system_code_dir": "", "design_docs_dir": "",
|
||||
}
|
||||
r = client.post("/api/projects", json=body)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["name"] == "stock"
|
||||
lst = client.get("/api/projects").json()
|
||||
assert any(p["name"] == "stock" for p in lst)
|
||||
one = client.get("/api/projects/stock").json()
|
||||
assert one["template"] == str(_SAMPLE / "template_design_ja.docx")
|
||||
|
||||
|
||||
def test_project_invalid_template_400(client):
|
||||
r = client.post("/api/projects", json={"name": "p", "template": "/no/such.docx"})
|
||||
assert r.status_code == 400
|
||||
assert r.json()["detail"]["code"] == "PROJECT_CONFIG_INVALID"
|
||||
|
||||
|
||||
def test_delete_project(client):
|
||||
client.post("/api/projects", json={"name": "p", "template": str(_SAMPLE / "template_design_ja.docx")})
|
||||
assert client.delete("/api/projects/p").json()["deleted"] is True
|
||||
assert client.get("/api/projects/p").status_code == 404
|
||||
|
||||
|
||||
def test_session_accepts_name_and_project(client):
|
||||
r = client.post("/api/sessions", json={"user_id": "u1", "name": "我的会话", "project": "projA"})
|
||||
assert r.status_code == 200
|
||||
sid = r.json()["session_id"]
|
||||
assert r.json()["name"] == "我的会话"
|
||||
g = client.get(f"/api/sessions/{sid}").json()
|
||||
assert g["name"] == "我的会话"
|
||||
assert g["project"] == "projA"
|
||||
|
||||
|
||||
def test_session_list_includes_name_project(client):
|
||||
client.post("/api/sessions", json={"user_id": "u1", "name": "n1", "project": "pA"})
|
||||
r = client.get("/api/sessions", params={"user_id": "u1"}).json()
|
||||
assert any(s["name"] == "n1" and s["project"] == "pA" for s in r)
|
||||
|
||||
|
||||
def test_generate_with_project_config_no_template_upload(client):
|
||||
"""绑定项目(含模板/规则)后,仅上传要件定义即可解析生成。"""
|
||||
client.post("/api/projects", json={
|
||||
"name": "projA", "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": "",
|
||||
})
|
||||
sid = client.post("/api/sessions", json={"user_id": "u1", "project": "projA"}).json()["session_id"]
|
||||
# 仅上传要件定义
|
||||
r = client.post(f"/api/sessions/{sid}/files", data={"file_type": "requirements"},
|
||||
files={"file": ("requirements_newdev.xlsx", (_SAMPLE / "requirements_newdev.xlsx").read_bytes())})
|
||||
assert r.status_code == 200
|
||||
p = client.post(f"/api/sessions/{sid}/start-parse")
|
||||
assert p.status_code == 200, p.text
|
||||
assert client.post(f"/api/sessions/{sid}/confirm-parse").status_code == 200
|
||||
# 无既有系统 → 直接 writing,可生成
|
||||
gen = client.post(f"/api/sessions/{sid}/generate", json={})
|
||||
assert gen.status_code == 200, gen.text
|
||||
assert client.get(f"/api/sessions/{sid}/result/download").status_code == 200
|
||||
|
||||
@@ -9,7 +9,7 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from genesis.server.store import SessionStore
|
||||
from genesis.server.store import SessionStore, ProjectsStore
|
||||
from genesis.server.service import GenesisService, ServiceStepError, FileTypeError
|
||||
|
||||
_SAMPLE = Path(__file__).resolve().parents[1] / "sample"
|
||||
@@ -59,6 +59,104 @@ def test_create_and_upload_files(svc):
|
||||
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):
|
||||
|
||||
+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="")
|
||||
|
||||
@@ -54,6 +54,14 @@ def test_parse_missing_requirement_file(tmp_path):
|
||||
SourceParser().parse(requirement_paths=[tmp_path / "missing.xlsx"])
|
||||
|
||||
|
||||
def test_parse_design_docs_collected_as_design_category(tmp_path):
|
||||
xlsx = _xlsx(tmp_path)
|
||||
design = make_rule_doc(tmp_path, [("H1", "設計書:注文管理の全体方針")])
|
||||
result = SourceParser().parse(requirement_paths=[xlsx], design_doc_paths=[design])
|
||||
assert len(result.design_docs) == 1
|
||||
assert result.design_docs[0].category == "design"
|
||||
|
||||
|
||||
def test_parse_missing_template_file(tmp_path):
|
||||
xlsx = _xlsx(tmp_path)
|
||||
with pytest.raises(FileNotFoundError):
|
||||
|
||||
Reference in New Issue
Block a user