- Writer: 表格表头行/Table Grid 边框、列表 List Bullet/Number 样式、行内字符格式不再塌缩(外视 #6 反转) - Writer: 打通章间引用(WriterState 摘要 → 后章 prompt prior_summaries) - Writer: 删除 _chunk_source 死代码,章节数据经 DataGate 控 token 预算 - Writer: 章节结果落盘快照,命中即跳过 LLM(录制重拍可续跑,损坏快照自动忽略) - Parser: 按 body 顺序遍历正文+单元格(含嵌套表、合并单元格去重),修复表格内锚点漏检导致的静默丢章 - Parser/服务层: .xls 显式拒绝(可操作提示),上传即校验扩展名,rules 对齐 docx-only - 测试: 全量 680 通过,覆盖率 99.37%(红线 99%)
253 lines
8.4 KiB
Python
253 lines
8.4 KiB
Python
import pytest
|
||
from pathlib import Path
|
||
|
||
from genesis.data_models import StructuredSource
|
||
from genesis.impact.code_parser import CodeParseError
|
||
from genesis.parsers.source_aggregator import SourceParser
|
||
|
||
from tests.docx_helpers import make_rule_doc, new_document, save_document
|
||
from tests.excel_helpers import new_workbook, save_workbook
|
||
|
||
|
||
def _xlsx(tmp_path, name: str = "source.xlsx") -> str:
|
||
wb = new_workbook({"機能一覧": [["機能ID", "機能名"], ["A001", "社員登録"]]})
|
||
path = tmp_path / name
|
||
wb.save(path)
|
||
return str(path)
|
||
|
||
|
||
def test_parse_full_assembly(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
template = save_document(tmp_path, new_document())
|
||
rule = make_rule_doc(tmp_path, [("H1", "1. 機能一覧の書き方")])
|
||
instr = make_rule_doc(tmp_path, [("H1", "2. 機能一覧の作成手順")])
|
||
|
||
result = SourceParser().parse(
|
||
requirement_paths=[xlsx],
|
||
template_path=template,
|
||
write_instruction_paths=[instr],
|
||
rule_paths=[rule],
|
||
)
|
||
|
||
assert isinstance(result, StructuredSource)
|
||
assert len(result.tables) == 1
|
||
assert result.template is not None
|
||
assert result.template.file_name == "source.docx"
|
||
assert len(result.rule_docs) == 2
|
||
assert all(r.category == "write" for r in result.rule_docs)
|
||
assert result.image_analyses == []
|
||
assert result.existing_system is None
|
||
|
||
|
||
def test_parse_without_template_and_rules(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
|
||
result = SourceParser().parse(requirement_paths=[xlsx])
|
||
|
||
assert len(result.tables) == 1
|
||
assert result.template is None
|
||
assert result.rule_docs == []
|
||
|
||
|
||
def test_parse_missing_requirement_file(tmp_path):
|
||
with pytest.raises(FileNotFoundError):
|
||
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):
|
||
SourceParser().parse(requirement_paths=[xlsx], template_path=tmp_path / "missing.docx")
|
||
|
||
|
||
def test_parse_unknown_extension_in_requirements(tmp_path):
|
||
bad = tmp_path / "note.txt"
|
||
bad.write_text("hello", encoding="utf-8")
|
||
with pytest.raises(ValueError, match="不支持的文件类型"):
|
||
SourceParser().parse(requirement_paths=[bad])
|
||
|
||
|
||
def test_parse_unknown_extension_in_rules(tmp_path):
|
||
bad = tmp_path / "note.txt"
|
||
bad.write_text("hello", encoding="utf-8")
|
||
with pytest.raises(ValueError, match="不支持的文件类型"):
|
||
SourceParser().parse(rule_paths=[bad])
|
||
|
||
|
||
def test_parse_unknown_extension_in_template(tmp_path):
|
||
bad = tmp_path / "note.txt"
|
||
bad.write_text("hello", encoding="utf-8")
|
||
with pytest.raises(ValueError, match="不支持的文件类型"):
|
||
SourceParser().parse(template_path=bad)
|
||
|
||
|
||
def test_parse_missing_rule_file(tmp_path):
|
||
with pytest.raises(FileNotFoundError):
|
||
SourceParser().parse(rule_paths=[tmp_path / "missing.docx"])
|
||
|
||
|
||
def test_parse_extensionless_requirement(tmp_path):
|
||
bad = tmp_path / "note"
|
||
bad.write_text("hello", encoding="utf-8")
|
||
with pytest.raises(ValueError, match="无扩展名"):
|
||
SourceParser().parse(requirement_paths=[bad])
|
||
|
||
|
||
def test_parse_extensionless_rule(tmp_path):
|
||
bad = tmp_path / "note"
|
||
bad.write_text("hello", encoding="utf-8")
|
||
with pytest.raises(ValueError, match="无扩展名"):
|
||
SourceParser().parse(rule_paths=[bad])
|
||
|
||
|
||
# ---------- P2-2:旧版 .xls 显式拒绝(可操作消息,非含糊格式错误) ----------
|
||
|
||
def test_parse_legacy_xls_requirement_rejected_actionably(tmp_path):
|
||
bad = tmp_path / "old.xls"
|
||
bad.write_bytes(b"legacy-biff")
|
||
with pytest.raises(ValueError, match="另存为 .xlsx"):
|
||
SourceParser().parse(requirement_paths=[bad])
|
||
|
||
|
||
def test_xlsx_exts_excludes_legacy_xls():
|
||
from genesis.parsers.source_aggregator import LEGACY_EXCEL_EXTS, XLSX_EXTS
|
||
assert XLSX_EXTS == (".xlsx",)
|
||
assert ".xls" in LEGACY_EXCEL_EXTS
|
||
|
||
|
||
# ---------- T7: DRY _validate_path helper(I8) ----------
|
||
|
||
def test_validate_path_rejects_bad_extension(tmp_path):
|
||
from genesis.parsers.source_aggregator import _validate_path
|
||
|
||
bad = tmp_path / "note.txt"
|
||
bad.write_text("x", encoding="utf-8")
|
||
with pytest.raises(ValueError, match="不支持的文件类型"):
|
||
_validate_path(bad, (".xlsx",))
|
||
|
||
|
||
def test_validate_path_rejects_missing_file(tmp_path):
|
||
from genesis.parsers.source_aggregator import _validate_path
|
||
|
||
with pytest.raises(FileNotFoundError):
|
||
_validate_path(tmp_path / "missing.xlsx", (".xlsx",))
|
||
|
||
|
||
def test_validate_path_returns_resolved_path(tmp_path):
|
||
from genesis.parsers.source_aggregator import _validate_path
|
||
|
||
good = tmp_path / "ok.xlsx"
|
||
good.write_bytes(b"x")
|
||
result = _validate_path(str(good), (".xlsx",))
|
||
assert isinstance(result, Path)
|
||
assert result.suffix.lower() == ".xlsx"
|
||
|
||
|
||
# ---------- Impact Agent MVP:existing_system_path(2026-08-23) ----------
|
||
|
||
JAVA_PROJECT = {
|
||
"trade-order/OrderController.java": (
|
||
"package com.trade.order.controller;\n"
|
||
"import org.springframework.web.bind.annotation.*;\n"
|
||
"@RestController\n"
|
||
"@RequestMapping(\"/api/order\")\n"
|
||
"public class OrderController {\n"
|
||
" @GetMapping(\"/{id}\")\n"
|
||
" public String get(Long id) { return \"ok\"; }\n"
|
||
"}\n"
|
||
),
|
||
"trade-order/OrderServiceImpl.java": (
|
||
"package com.trade.order.service;\n"
|
||
"import org.springframework.stereotype.Service;\n"
|
||
"@Service\n"
|
||
"public class OrderServiceImpl {\n"
|
||
" public void createOrder() {}\n"
|
||
"}\n"
|
||
),
|
||
"trade-order/OrderDO.java": (
|
||
"package com.trade.order.entity;\n"
|
||
"import com.baomidou.mybatisplus.annotation.TableName;\n"
|
||
"@TableName(\"trade_order\")\n"
|
||
"public class OrderDO {\n"
|
||
" private Long id;\n"
|
||
"}\n"
|
||
),
|
||
}
|
||
|
||
|
||
def _java_project(tmp_path) -> str:
|
||
for rel, content in JAVA_PROJECT.items():
|
||
p = tmp_path / "existing" / rel
|
||
p.parent.mkdir(parents=True, exist_ok=True)
|
||
p.write_text(content, encoding="utf-8")
|
||
return str(tmp_path / "existing")
|
||
|
||
|
||
def test_parse_with_existing_system_path(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
existing = _java_project(tmp_path)
|
||
|
||
result = SourceParser().parse(requirement_paths=[xlsx], existing_system_path=existing)
|
||
|
||
assert result.existing_system is not None
|
||
names = {c.class_name for c in result.existing_system.controller_layer}
|
||
assert "OrderController" in names
|
||
assert {s.class_name for s in result.existing_system.service_layer} == {"OrderServiceImpl"}
|
||
assert {e.class_name for e in result.existing_system.entity_layer} == {"OrderDO"}
|
||
|
||
|
||
def test_parse_without_existing_system_path_keeps_none(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
result = SourceParser().parse(requirement_paths=[xlsx])
|
||
assert result.existing_system is None
|
||
|
||
|
||
def test_parse_existing_system_path_invalid_dir_raises(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
with pytest.raises(CodeParseError):
|
||
SourceParser().parse(requirement_paths=[xlsx], existing_system_path=tmp_path / "nope")
|
||
|
||
|
||
def test_parse_existing_system_path_without_java_raises(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
empty = tmp_path / "empty"
|
||
empty.mkdir()
|
||
with pytest.raises(CodeParseError):
|
||
SourceParser().parse(requirement_paths=[xlsx], existing_system_path=empty)
|
||
|
||
|
||
# ---------- 多语言预备重构:existing_system_language 透传 ----------
|
||
|
||
def test_parse_existing_system_language_explicit(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
existing = _java_project(tmp_path)
|
||
|
||
result = SourceParser().parse(
|
||
requirement_paths=[xlsx],
|
||
existing_system_path=existing,
|
||
existing_system_language="java",
|
||
)
|
||
|
||
assert result.existing_system is not None
|
||
assert {c.class_name for c in result.existing_system.controller_layer} == {"OrderController"}
|
||
|
||
|
||
def test_parse_existing_system_language_unsupported_raises(tmp_path):
|
||
xlsx = _xlsx(tmp_path)
|
||
existing = _java_project(tmp_path)
|
||
|
||
with pytest.raises(CodeParseError):
|
||
SourceParser().parse(
|
||
requirement_paths=[xlsx],
|
||
existing_system_path=existing,
|
||
existing_system_language="kotlin",
|
||
)
|