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:
@@ -598,6 +598,18 @@ class TestActionsManifest(unittest.TestCase):
|
||||
msg=f"{action['id']} must not be auto-placed on toolbar",
|
||||
)
|
||||
|
||||
def test_broadcast_monthly_is_async_with_cron_and_agent(self) -> None:
|
||||
"""定位表(开发文档 §9 / REQUIREMENTS §0.1):月度播报必须 async + agent + cron。"""
|
||||
manifest = _load_actions_manifest()
|
||||
monthly = next((a for a in manifest["actions"] if a["id"] == "broadcast-monthly"), None)
|
||||
self.assertIsNotNone(monthly, msg="broadcast-monthly action missing from manifest")
|
||||
self.assertEqual(monthly.get("executionProfile"), "async")
|
||||
placements = set(monthly.get("placements") or [])
|
||||
self.assertIn("cron", placements, msg="broadcast-monthly must be placed on cron (每月 5 号定时)")
|
||||
self.assertIn("agent", placements, msg="broadcast-monthly must be reachable from conversation")
|
||||
entry = monthly.get("entrypoint") or {}
|
||||
self.assertEqual(entry.get("command"), "broadcast-monthly")
|
||||
|
||||
def test_actions_md_documents_strict_vs_host_boundary(self) -> None:
|
||||
path = os.path.join(get_skill_root(), "references", "ACTIONS.md")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
|
||||
@@ -37,6 +37,70 @@ class TestDbSmoke(unittest.TestCase):
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_init_db_migrates_legacy_progress_reports(self) -> None:
|
||||
"""v1.0.1 旧库(无 items_json/updated_at)→ init_db 重建迁移且数据不丢。"""
|
||||
with IsolatedDataRoot():
|
||||
from db.connection import get_conn, init_db
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE progress_reports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL,
|
||||
project_id INTEGER,
|
||||
project_no TEXT,
|
||||
project_name TEXT,
|
||||
pi_name TEXT,
|
||||
as_of TEXT,
|
||||
spend_rate REAL,
|
||||
time_progress REAL,
|
||||
deviation REAL,
|
||||
status_tag TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
suggestions_json TEXT,
|
||||
report_file TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO progress_reports (batch_no, project_no, status_tag) VALUES (?, ?, ?)",
|
||||
("legacy-batch", "OLD001", "严重滞后"),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
init_db()
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("PRAGMA table_info(progress_reports)")
|
||||
cols = [str(row[1]) for row in cur.fetchall()]
|
||||
self.assertIn("items_json", cols)
|
||||
self.assertIn("updated_at", cols)
|
||||
|
||||
row = cur.execute(
|
||||
"SELECT project_no, status_tag FROM progress_reports WHERE batch_no = ?",
|
||||
("legacy-batch",),
|
||||
).fetchone()
|
||||
self.assertEqual(row, ("OLD001", "严重滞后"))
|
||||
|
||||
leftovers = cur.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '_progress%'"
|
||||
).fetchall()
|
||||
self.assertEqual(leftovers, [])
|
||||
|
||||
triggers = cur.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='progress_reports'"
|
||||
).fetchall()
|
||||
self.assertTrue(any("set_updated_at" in str(t[0]) for t in triggers))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -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