Files
2026Technology-Competition/tests/test_phase5_models.py
T
lhl 0a498e4f7a fix(writer): 无锚点子章归并父章生成 + 注入后裸重复子节去重
- template_mapper: 按 design §6.5 仅 Heading level<=1 起章,H2/H3 归入
  父章 sub_headings(此前每个 heading 独立成章,无 {{section:id}} 锚点的
  6 个子章生成后被静默丢弃)
- models: ChapterSpec.sub_headings 字段;to_vars 暴露 sub_headings 变量
- writer_agent: prompt 新增【小节约束】(有子节时按小节顺序以 level=2
  heading 组织,不得遗漏或新增)
- docx_injector: 注入后删除同章内与已生成标题同名且完全无内容的模板裸
  H2/H3(保守策略:带内容的模板子节保留)
- 真实试运行验证:7 章 / 无静默丢弃告警 / 14 个 H2 无重复
2026-08-24 12:23:35 +08:00

245 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from genesis.data_models import (
CellValue,
ExcelTable,
ParsedTemplate,
ChapterMarker,
Provenance,
SheetType,
StructuredSource,
)
from genesis.writer.models import ContentBlock, ChapterContent, GenerationContext, ChapterSpec
def _cell(v):
return CellValue(value=v, provenance=Provenance("要件.xlsx", "s1", 1, "A", "列"))
def _table(name, sheet_type, headers, rows):
return ExcelTable(
name=name,
detected_type=sheet_type,
extraction_method="openpyxl",
headers=headers,
rows=[{h: _cell(v) for h, v in zip(headers, row)} for row in rows],
)
def _mk_source():
"""含 FUNCTION / DATABASE / GENERIC 三张表的 StructuredSource(模板仅占位)。"""
template = ParsedTemplate(
file_name="t.docx",
sections=[ChapterMarker(type="heading", name="x", level=1)],
placeholders={},
styles={"used": []},
)
return StructuredSource(
tables=[
_table("機能一覧", SheetType.FUNCTION, ["機能ID", "機能名"], [["F001", "止损风控"], ["F002", "订单登录"]]),
_table("DB定義", SheetType.DATABASE, ["テーブルID", "テーブル名"], [["T001", "trade_order"]]),
_table("自由記述", SheetType.GENERIC, ["text"], [["系统采用前后端分离架构"]]),
],
template=template,
rule_docs=[],
image_analyses=[],
existing_system=None,
comments=[],
)
_UNSET = object()
def _ctx(cid, title, source=_UNSET, report=None):
return GenerationContext(
chapter_id=cid, title=title,
template_marker=ChapterSpec(chapter_id=cid, title=title, section_placeholder=f"section:{cid}"),
structured_source=None if source is None else (
_mk_source() if source is _UNSET else source
),
write_rules=[], design_rules=[], template_styles=set(),
impact_report=report,
)
def test_content_block_defaults():
b = ContentBlock(block_id="b1", type="paragraph", text="你好")
assert b.level is None
assert b.source_uris == []
def test_chapter_content_holds_blocks():
b = ContentBlock(block_id="b1", type="heading", level=2, text="标题")
c = ChapterContent(chapter_id="db_design", version=1, title="DB 设计", blocks=[b])
assert c.blocks[0].type == "heading"
assert c.version == 1
def test_generation_context_impact_field_default_none():
ctx = GenerationContext(
chapter_id="db_design", title="DB 设计",
template_marker=ChapterSpec(chapter_id="db_design", title="DB 设计", section_placeholder="{{section:db_design}}"),
structured_source=None, write_rules=["规则1"], design_rules=["规则2"],
template_styles={"Heading 1"},
)
assert ctx.impact_report is None
# to_vars 始终提供 impact 变量;无影响调查书时为空串
assert ctx.to_vars()["impact"] == ""
def test_generation_context_impact_var_formats_report():
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[ChangeElement("F001", "機能", "止损风控", ChangeType.NEW)],
modified_elements=[],
deleted_elements=[],
unchanged_elements=[],
warnings=[],
)
report = ImpactReport(metadata={"version": "v1"}, change_analysis=ca, summary={"new": 1})
# function_list 章节对应 ElementType=機能 → 该要素应保留
ctx = _ctx("function_list", "機能一覧", source=None, report=report)
impact = ctx.to_vars()["impact"]
assert "止损风控" in impact
assert "F001" in impact
def _impact_ctx(report):
# function_list 章节对应 ElementType=機能 → [削除] 機能要素可保留
return _ctx("function_list", "機能一覧", source=None, report=report)
def test_impact_var_empty_when_report_without_analysis():
from genesis.data_models import ImpactReport
report = ImpactReport(metadata={"version": "v1"}, change_analysis=None, summary={})
assert _impact_ctx(report).to_vars()["impact"] == ""
def test_impact_var_formats_deleted_and_warnings():
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport, ImpactWarning
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[],
modified_elements=[],
deleted_elements=[ChangeElement("F030", "機能", "旧功能", ChangeType.DELETED)],
unchanged_elements=[],
warnings=[ImpactWarning("F030", "缺少既存対応")],
)
report = ImpactReport(metadata={}, change_analysis=ca, summary={"deleted": 1, "warnings": 1})
impact = _impact_ctx(report).to_vars()["impact"]
assert "[削除]" in impact and "旧功能" in impact
assert "[警告]" in impact and "缺少既存対応" in impact
def test_chapter_spec_placeholder_optional():
s = ChapterSpec(chapter_id="x", title="X", section_placeholder=None)
assert s.section_placeholder is None
# ---------- 章节级数据注入(design.md §6.8 ①:selector=该章数据) ----------
def test_to_vars_data_scoped_by_chapter():
"""db_design 章的 data 只含 DATABASE 表 + GENERIC 自由記述,不含 FUNCTION 表。"""
data = _ctx("db_design", "DB設計").to_vars()["data"]
assert "テーブルID" in data and "trade_order" in data # DB 表内容
assert "前后端分离" in data # GENERIC 通用背景
assert "止损风控" not in data and "機能ID" not in data # FUNCTION 表被排除
def test_to_vars_data_function_chapter_gets_function_table():
data = _ctx("function_list", "機能一覧").to_vars()["data"]
assert "F001" in data and "止损风控" in data
assert "trade_order" not in data # DB 表被排除
def test_to_vars_data_introduction_gets_all_tables():
"""introduction 概览章注入全部类型表。"""
data = _ctx("introduction", "はじめに").to_vars()["data"]
assert "F001" in data and "trade_order" in data
def test_to_vars_data_empty_when_no_matching_table():
"""无对应类型表(如 report_list 无 REPORT 表)→ 仅 GENERIC 背景,不报错。"""
data = _ctx("report_list", "帳票一覧").to_vars()["data"]
assert "帳票ID" not in data
assert "前后端分离" in data # GENERIC 始终保留
def test_to_vars_data_none_safe_and_source_key_removed():
"""structured_source=None → data 为空串;旧的全量 source 键不再提供。"""
vars_ = _ctx("db_design", "DB設計", source=None).to_vars()
assert vars_["data"] == ""
assert "source" not in vars_
def test_to_vars_data_renders_markdown_table_shape():
"""表渲染为可读 Markdown 形态(表名行 + 表头行 + 分隔行)。"""
data = _ctx("db_design", "DB設計").to_vars()["data"]
assert "DB定義" in data
assert "| テーブルID | テーブル名 |" in data
assert "---" in data
assert "| T001 | trade_order |" in data
def test_impact_filtered_by_chapter_element_type():
"""db_design 章 impact 只显示 DB 要素;機能要素被过滤。"""
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[
ChangeElement("T001", "DB", "订单表", ChangeType.NEW),
ChangeElement("F001", "機能", "止损风控", ChangeType.NEW),
],
modified_elements=[],
deleted_elements=[],
unchanged_elements=[],
warnings=[],
)
report = ImpactReport(metadata={}, change_analysis=ca, summary={"new": 2})
impact = _ctx("db_design", "DB設計", source=None, report=report).to_vars()["impact"]
assert "T001" in impact and "订单表" in impact
assert "F001" not in impact and "止损风控" not in impact
def test_impact_introduction_shows_all_elements():
"""introduction 章(None 过滤)展示全部要素。"""
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[ChangeElement("T001", "DB", "订单表", ChangeType.NEW),
ChangeElement("F001", "機能", "止损风控", ChangeType.NEW)],
modified_elements=[], deleted_elements=[], unchanged_elements=[], warnings=[],
)
report = ImpactReport(metadata={}, change_analysis=ca, summary={"new": 2})
impact = _ctx("introduction", "はじめに", source=None, report=report).to_vars()["impact"]
assert "T001" in impact and "F001" in impact
# ---------- 子节结构注入(design.md §6.5H2/H3 归入本章) ----------
def test_chapter_spec_sub_headings_default_empty():
s = ChapterSpec(chapter_id="x", title="X")
assert s.sub_headings == []
def test_to_vars_exposes_sub_headings():
spec = ChapterSpec(
chapter_id="function_list", title="2. 機能一覧",
section_placeholder="section:function_list",
sub_headings=["2.1 機能一覧表", "2.2 機能詳細"],
)
ctx = GenerationContext(
chapter_id=spec.chapter_id, title=spec.title,
template_marker=spec,
structured_source=None, write_rules=[], design_rules=[],
template_styles=set(),
)
v = ctx.to_vars()
assert "2.1 機能一覧表" in v["sub_headings"]
assert "2.2 機能詳細" in v["sub_headings"]
def test_to_vars_sub_headings_empty_when_none():
vars_ = _ctx("db_design", "DB設計").to_vars()
assert vars_["sub_headings"] == ""