fix: 对照《财务处Skill002》验收整改并升版 v1.0.2
All checks were successful
技能自动化发布 / release (push) Successful in 9s
All checks were successful
技能自动化发布 / release (push) Successful in 9s
- 状态标签:C3 触发且 D 未判 → 待补录,优先级对齐需求 §6.2 - broadcast-monthly 逐项目增量 emit,任务中心可见进行中进度 - progress_reports 回填 project_id;新增 items_json/updated_at;旧库重建式安全迁移(含回归测试) - 简报:TITLE 转义防注入、章节对齐 §10(独立「暂付款/结题提醒」)、页脚逐字统一 - 金标准测试 +17:±10%/20% 阈值恰界、C2 钳位、N2 反例、C3 逐字段、HTML 转义、async+cron 定位守护、迁移回归 - 文档 v1.1(开发文档 + REQUIREMENTS):ERROR:<CODE> <message>、reports_brief、--all-status、skill-detail 表外挂载、C1 level=warn - 落盘验收报告 acceptance-reports/20260924-105401-acceptance.md(338 测试全绿) - 版本 1.0.1 → 1.0.2(constants / SKILL.md / CHANGELOG 三处对齐)
This commit is contained in:
@@ -179,6 +179,80 @@ class TestBroadcastKernel(unittest.TestCase):
|
||||
brief_html = open(result["brief_file"], encoding="utf-8").read()
|
||||
self.assertLess(brief_html.find("严重滞后"), brief_html.find("正常"))
|
||||
|
||||
def test_on_progress_called_per_project(self) -> None:
|
||||
from service.fund_service import run_broadcast
|
||||
|
||||
seen: list[tuple[int, int, str]] = []
|
||||
result = run_broadcast(
|
||||
[_project(), _project(projectNo="TEST0002")],
|
||||
AS_OF,
|
||||
on_progress=lambda i, t, r: seen.append((i, t, r["project_no"])),
|
||||
)
|
||||
self.assertEqual(seen, [(1, 2, "TEST0001"), (2, 2, "TEST0002")])
|
||||
self.assertEqual(result["count"], 2)
|
||||
|
||||
def test_report_links_project_id_and_items_json(self) -> None:
|
||||
import json
|
||||
|
||||
from db.connection import get_conn
|
||||
from service.fund_service import query_projects, run_broadcast
|
||||
from service.project_service import upsert_project
|
||||
|
||||
upsert_project({
|
||||
"project_no": "TEST0001",
|
||||
"total_received": 300000.0,
|
||||
"total_spent": 60000.0,
|
||||
"start_date": "2026-01-01",
|
||||
"end_date": "2026-12-31",
|
||||
"frozen_amount": 15000.0,
|
||||
"frozen_since": "2026-04-10",
|
||||
})
|
||||
projects = query_projects(include_all_status=True)
|
||||
self.assertEqual(len(projects), 1)
|
||||
run_broadcast(projects, AS_OF)
|
||||
conn = get_conn()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT project_id, items_json FROM progress_reports WHERE project_no = ?",
|
||||
("TEST0001",),
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
self.assertEqual(row[0], projects[0]["id"])
|
||||
items = json.loads(row[1])
|
||||
self.assertTrue(any(i["rule_id"] == "D1" for i in items))
|
||||
self.assertTrue(any(i["rule_id"] == "N1" for i in items))
|
||||
|
||||
def test_report_escapes_html_injection(self) -> None:
|
||||
result = self._run([_project(projectName="<script>alert(1)</script>")])
|
||||
with open(result["reports"][0]["report_file"], encoding="utf-8") as f:
|
||||
html_text = f.read()
|
||||
self.assertNotIn("<script>alert(1)</script>", html_text)
|
||||
self.assertIn("<script>", html_text)
|
||||
|
||||
def test_report_contains_status_lights_and_reminders(self) -> None:
|
||||
result = self._run([
|
||||
_project(projectNo="LAG001", frozenAmount=20000.0, frozenSince="2026-01-20"),
|
||||
_project(projectNo="FAST001", totalReceived=400000.0, totalSpent=380000.0),
|
||||
_project(projectNo="OK001", totalReceived=400000.0, totalSpent=350000.0),
|
||||
_project(projectNo="CLO001", endDate="2026-10-15", totalReceived=100000.0, totalSpent=5000.0),
|
||||
])
|
||||
by_no = {r["project_no"]: r for r in result["reports"]}
|
||||
|
||||
def _read(project_no: str) -> str:
|
||||
with open(by_no[project_no]["report_file"], encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
lag_html = _read("LAG001")
|
||||
self.assertIn("🔴", lag_html)
|
||||
self.assertIn("冲销", lag_html)
|
||||
self.assertIn("暂付款 / 结题提醒", lag_html)
|
||||
self.assertIn("🟢", _read("OK001"))
|
||||
self.assertIn("🟠", _read("FAST001"))
|
||||
clo_html = _read("CLO001")
|
||||
self.assertIn("距执行期结束", clo_html)
|
||||
self.assertIn("暂付款 / 结题提醒", clo_html)
|
||||
|
||||
|
||||
class TestProjectService(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
@@ -258,5 +332,67 @@ class TestProjectService(unittest.TestCase):
|
||||
self.assertTrue(payload["lag_top"])
|
||||
|
||||
|
||||
class TestThresholdBoundaries(unittest.TestCase):
|
||||
"""验收 3:恰在 −10% / +20% 阈值边界的归类(默认阈值 0.10 / 0.20)。"""
|
||||
|
||||
def test_d1_exactly_minus_10_percent_is_normal(self) -> None:
|
||||
result = _compute(totalReceived=100000.0, totalSpent=60050.0)
|
||||
self.assertEqual(result["time_progress"], 0.7005)
|
||||
self.assertEqual(result["deviation"], -0.1)
|
||||
self.assertEqual(result["status_tag"], "正常") # 严格小于才判滞后
|
||||
|
||||
def test_d1_just_below_minus_10_percent_is_lag(self) -> None:
|
||||
result = _compute(totalReceived=100000.0, totalSpent=60040.0) # dev=-0.1001
|
||||
self.assertEqual(result["status_tag"], "严重滞后")
|
||||
|
||||
def test_d2_exactly_plus_20_percent_is_normal(self) -> None:
|
||||
result = _compute(totalReceived=100000.0, totalSpent=90050.0)
|
||||
self.assertEqual(result["deviation"], 0.2)
|
||||
self.assertEqual(result["status_tag"], "正常")
|
||||
|
||||
def test_d2_just_above_plus_20_percent_is_overshoot(self) -> None:
|
||||
result = _compute(totalReceived=100000.0, totalSpent=90060.0) # dev=+0.2001
|
||||
self.assertEqual(result["status_tag"], "支出异常偏快")
|
||||
|
||||
|
||||
class TestC2Clamp(unittest.TestCase):
|
||||
def test_before_start_clamps_to_zero(self) -> None:
|
||||
result = _compute(startDate="2026-10-01", endDate="2026-12-31")
|
||||
self.assertEqual(result["time_progress"], 0.0)
|
||||
|
||||
def test_after_end_clamps_to_one(self) -> None:
|
||||
result = _compute(startDate="2026-01-01", endDate="2026-08-31")
|
||||
self.assertEqual(result["time_progress"], 1.0)
|
||||
|
||||
|
||||
class TestC3StatusPriority(unittest.TestCase):
|
||||
"""状态标签优先级:严重滞后>偏快>正常(D3 已判)>待补录(C3)>未到账(C1)。"""
|
||||
|
||||
def test_missing_end_date_marks_incomplete(self) -> None:
|
||||
result = _compute(endDate=None)
|
||||
self.assertIsNotNone(result["spend_rate"])
|
||||
self.assertEqual(result["status_tag"], "待补录")
|
||||
|
||||
def test_missing_start_date_marks_incomplete(self) -> None:
|
||||
result = _compute(startDate=None)
|
||||
self.assertEqual(result["status_tag"], "待补录")
|
||||
|
||||
def test_missing_total_received_marks_incomplete(self) -> None:
|
||||
result = _compute(totalReceived=None)
|
||||
self.assertEqual(result["status_tag"], "待补录")
|
||||
|
||||
def test_missing_total_approved_keeps_normal_when_d3_judged(self) -> None:
|
||||
result = _compute(totalApproved=None, totalReceived=400000.0, totalSpent=350000.0)
|
||||
self.assertEqual(result["status_tag"], "正常") # 正常>待补录
|
||||
c3 = next(i for i in result["items"] if i["rule_id"] == "C3")
|
||||
self.assertIn("总立项经费", c3["detail"])
|
||||
|
||||
|
||||
class TestN2Negative(unittest.TestCase):
|
||||
def test_closing_soon_but_rate_at_least_80pct_no_reminder(self) -> None:
|
||||
result = _compute(endDate="2026-10-15", totalReceived=100000.0, totalSpent=80000.0)
|
||||
self.assertFalse(any(i["rule_id"] == "N2" for i in result["items"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user