fix: 预览按 draft-{id} 命名且重建全量同构 + C1 空台账改判数据待补 + 立项经费结余结论 + CLI 拆分 import-*/list-* + docx 占位黄底
This commit is contained in:
@@ -92,6 +92,21 @@ class TestC1Completion(DraftKernelTestBase):
|
||||
result = self._kernel(expected=[{"type": "论文", "unit": "篇"}])
|
||||
self.assertEqual(result["completion"][0]["verdict"], "数据待补")
|
||||
|
||||
def test_empty_ledger_is_pending_not_unmet(self) -> None:
|
||||
"""成果台账整表为空 → 全部"数据待补",不下"未达标"结论(C1/W1)。"""
|
||||
result = self._kernel(achievements=[])
|
||||
for row in result["completion"]:
|
||||
self.assertEqual(row["verdict"], "数据待补")
|
||||
self.assertIn("尚未录入", row["detail"])
|
||||
|
||||
def test_present_ledger_missing_type_is_unmet_zero(self) -> None:
|
||||
"""台账非空但某类型无记录 → 实际按 0 计,未达标(事实陈述)。"""
|
||||
result = self._kernel(achievements=[ach for ach in _achievements() if ach["achievementType"] != "专利"])
|
||||
by_type = {row["type"]: row for row in result["completion"]}
|
||||
self.assertEqual(by_type["专利"]["verdict"], "未达标")
|
||||
self.assertEqual(by_type["专利"]["actual"], 0)
|
||||
self.assertIn("实际 0 项", by_type["专利"]["detail"])
|
||||
|
||||
|
||||
class TestC3Expenses(DraftKernelTestBase):
|
||||
def test_ratio_sums_to_one_and_sentences(self) -> None:
|
||||
@@ -106,6 +121,27 @@ class TestC3Expenses(DraftKernelTestBase):
|
||||
result = self._kernel(expenses=[])
|
||||
self.assertIn("尚未录入", result["expense_paragraph"])
|
||||
|
||||
def test_balance_within_approved_budget(self) -> None:
|
||||
# 台账支出 273000 元,立项经费 500000 元 → 结余 22.70 万元
|
||||
result = self._kernel()
|
||||
total = round(sum(row["amount"] for row in result["expense_rows"]), 2)
|
||||
self.assertEqual(total, 273000.0)
|
||||
with_approved = self._kernel_with_approved(500000)
|
||||
self.assertIn("合计支出 27.30 万元", with_approved)
|
||||
self.assertIn("结余 22.70 万元", with_approved)
|
||||
self.assertIn("范围内", with_approved)
|
||||
|
||||
def test_overspend_flags_exceed(self) -> None:
|
||||
paragraph = self._kernel_with_approved(200000)
|
||||
self.assertIn("超出立项经费", paragraph)
|
||||
self.assertIn("7.30 万元", paragraph)
|
||||
|
||||
def _kernel_with_approved(self, approved: float) -> str:
|
||||
from service.draft_service import build_draft_sections
|
||||
|
||||
project = _project(approvedBudget=approved)
|
||||
return build_draft_sections(project, _achievements(), _expenses())["expense_paragraph"]
|
||||
|
||||
|
||||
class TestC4AndC5(DraftKernelTestBase):
|
||||
def test_narrative_points_with_source(self) -> None:
|
||||
@@ -166,15 +202,42 @@ class TestDraftGeneration(unittest.TestCase):
|
||||
self.assertIn("经费决算说明", xml)
|
||||
self.assertIn("来源:成果清单", xml)
|
||||
self.assertIn("待补充", xml)
|
||||
self.assertIn('w:highlight w:val="yellow"', xml) # [待补充] 占位黄底提示
|
||||
|
||||
def test_preview_and_status_update(self) -> None:
|
||||
def test_preview_named_by_draft_id_and_status_update(self) -> None:
|
||||
result = self._draft()
|
||||
self.assertTrue(result["preview_file"].endswith(".html"))
|
||||
self.assertTrue(result["preview_file"].endswith("draft-1.html"))
|
||||
from service.ledger_service import list_projects
|
||||
|
||||
row = next(p for p in list_projects() if p["project_no"] == "NSFC2026003")
|
||||
self.assertEqual(row["status"], "初稿已生成")
|
||||
|
||||
def test_second_draft_preview_not_overwritten(self) -> None:
|
||||
from service.ledger_service import upsert_project
|
||||
from service.draft_service import query_pending_projects, run_draft
|
||||
|
||||
upsert_project(_project(projectNo="NSFC2026004", projectName="第二个项目"))
|
||||
result2 = run_draft(query_pending_projects(project_no="NSFC2026004")[0], [], [])
|
||||
result1 = run_draft(query_pending_projects(project_no="NSFC2026003")[0], [], [])
|
||||
self.assertIn("draft-1.html", result2["preview_file"])
|
||||
self.assertIn("draft-2.html", result1["preview_file"])
|
||||
self.assertNotEqual(result1["preview_file"], result2["preview_file"])
|
||||
|
||||
def test_rebuild_preview_is_full_sections(self) -> None:
|
||||
from service.report_service import build_preview_for_draft_id
|
||||
|
||||
result = self._draft()
|
||||
draft_id = result["draft_id"]
|
||||
path = build_preview_for_draft_id(draft_id)
|
||||
self.assertIn(f"draft-{draft_id}.html", path)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
html_text = f.read()
|
||||
# 与 Word 初稿同构:决算、成果清单、占位指引齐全(而非降级的三节摘要)
|
||||
self.assertIn("经费决算说明", html_text)
|
||||
self.assertIn("成果清单", html_text)
|
||||
self.assertIn("研究背景与目标", html_text)
|
||||
self.assertIn("待补充", html_text)
|
||||
|
||||
def test_batch_pending(self) -> None:
|
||||
from service.draft_service import query_pending_projects, run_draft
|
||||
|
||||
@@ -192,5 +255,42 @@ class TestDraftGeneration(unittest.TestCase):
|
||||
self.assertEqual(query_pending_projects(project_no="NOPE0001"), [])
|
||||
|
||||
|
||||
class TestCliContract(unittest.TestCase):
|
||||
"""CLI 契约:文档 §8 的六个台账命令可解析,render-html 占位符容错。"""
|
||||
|
||||
def _parser(self):
|
||||
from cli.app import build_parser
|
||||
|
||||
return build_parser()
|
||||
|
||||
def test_contract_commands_parse(self) -> None:
|
||||
parser = self._parser()
|
||||
cases = [
|
||||
["import-projects", "--projects-json", "x.json"],
|
||||
["import-achievements", "--achievements-json", "-"],
|
||||
["import-expenses", "--expenses-json", "x.json"],
|
||||
["list-projects", "--status", "待结题"],
|
||||
["list-achievements", "--project-no", "NSFC2026003"],
|
||||
["list-expenses", "--project-no", "NSFC2026003"],
|
||||
["add-project", "--project-no", "X", "--approved-budget", "500000"],
|
||||
]
|
||||
for argv in cases:
|
||||
args = parser.parse_args(argv)
|
||||
self.assertTrue(callable(getattr(args, "handler", None)), argv)
|
||||
|
||||
def test_legacy_import_list_aliases_kept(self) -> None:
|
||||
parser = self._parser()
|
||||
args = parser.parse_args(["import", "--projects-json", "x.json"])
|
||||
self.assertEqual(args.projects_json, "x.json")
|
||||
args = parser.parse_args(["list", "--kind", "expenses", "--project-no", "X"])
|
||||
self.assertEqual(args.kind, "expenses")
|
||||
|
||||
def test_render_html_unresolved_placeholder_falls_back(self) -> None:
|
||||
parser = self._parser()
|
||||
args = parser.parse_args(["render-html", "--report-id", "{{id}}"])
|
||||
# 未解析占位符不应让渲染崩溃:handler 内部按缺省(最近 10 份)处理
|
||||
self.assertEqual(args.report_id, "{{id}}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user