#!/usr/bin/env python3 # -*- coding: utf-8 -*- """默认测试套件:奖助学金评定逻辑防伪校验。 设计原则: - 仅用标准库(不依赖 pytest / jinja2 / requests / pandas),CI 与本地均能直接 `python tests/run_tests.py` 跑 - 不依赖 sqlite3(v1.0.1 骨架尚未引入 DB schema,全部测试在纯函数层) - 覆盖:util.constants 版本对齐 + SKILL.md 元数据对齐 + CLI 入口 dispatch - 退出码:0 = 全部通过,1 = 有失败 - -v 开关:详细输出每个用例的状态 release.ps1 在打 tag 前会自动跑 `python tests/run_tests.py -v`,任何一个 test_* 返回 (False, msg) 即 abort 发布。 """ from __future__ import annotations import argparse import io import re import sys from contextlib import redirect_stdout from pathlib import Path from typing import Any, Callable # 让 scripts/ 下的模块可导入 _REPO_ROOT = Path(__file__).resolve().parent.parent _SCRIPTS_DIR = _REPO_ROOT / "scripts" sys.path.insert(0, str(_SCRIPTS_DIR)) # ---------- 测试基础设施 ---------- _RESULTS: list[tuple[str, bool, str]] = [] _VERBOSE = False def _run(name: str, fn: Callable[[], tuple[bool, str]]) -> None: """运行单个 test_*,记录结果。""" try: ok, msg = fn() except Exception as e: import traceback tb = traceback.format_exc(limit=3) ok, msg = False, f"exception: {e}\n{tb}" _RESULTS.append((name, ok, msg)) if _VERBOSE or not ok: marker = "PASS" if ok else "FAIL" print(f" [{marker}] {name}: {msg}", flush=True) # ---------- util.constants 版本对齐(POLICY-RELEASE-001) ---------- def test_skill_md_version_alignment() -> tuple[bool, str]: """POLICY-RELEASE-001:SKILL.md version == SKILL_VERSION == CHANGELOG 顶部版本""" from util.constants import SKILL_VERSION skill_md = (_REPO_ROOT / "SKILL.md").read_text(encoding="utf-8") m = re.search(r'(?m)^version:\s*["\']?(\d+\.\d+\.\d+)', skill_md) if not m: return False, "SKILL.md 缺顶层 version: X.Y.Z" md_ver = m.group(1) if md_ver != SKILL_VERSION: return False, f"SKILL.md={md_ver} != SKILL_VERSION={SKILL_VERSION}" # CHANGELOG 顶部应有 ## X.Y.Z cl = (_REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8") cl_m = re.search(r'(?m)^##\s+(\d+\.\d+\.\d+)', cl) if not cl_m: return False, "CHANGELOG.md 缺顶层 ## X.Y.Z" cl_ver = cl_m.group(1) if cl_ver != SKILL_VERSION: return False, f"CHANGELOG 顶部={cl_ver} != SKILL_VERSION={SKILL_VERSION}" return True, f"SKILL.md={md_ver} == CHANGELOG={cl_ver} == SKILL_VERSION={SKILL_VERSION}" def test_skill_md_slug_alignment() -> tuple[bool, str]: """SKILL.md metadata.openclaw.slug == SKILL_SLUG""" from util.constants import SKILL_SLUG skill_md = (_REPO_ROOT / "SKILL.md").read_text(encoding="utf-8") m = re.search(r'(?m)^\s*slug:\s*["\']?([a-z0-9_-]+)', skill_md) if not m: return False, "SKILL.md 缺 metadata.openclaw.slug" md_slug = m.group(1) if md_slug != SKILL_SLUG: return False, f"SKILL.md slug={md_slug} != SKILL_SLUG={SKILL_SLUG}" return True, f"slug={md_slug}" def test_skill_md_brands_alignment() -> tuple[bool, str]: """SKILL.md metadata.openclaw.brands == constants.BRANDS(多平台发布对齐)""" from util.constants import BRANDS skill_md = (_REPO_ROOT / "SKILL.md").read_text(encoding="utf-8") # 在 SKILL.md 里找 brands: 后的列表内容 m = re.search( r'(?ms)^\s*brands:\s*(?:#[^\n]*\n)?(?P(?:\s*-\s*[a-z]+\s*[^\n]*\n)+)', skill_md, ) if not m: return False, "SKILL.md 缺 metadata.openclaw.brands 列表" listed = re.findall(r'-\s*([a-z0-9_-]+)', m.group("list")) listed_tuple = tuple(listed) if listed_tuple != BRANDS: return False, f"SKILL.md brands={listed_tuple} != BRANDS={BRANDS}" return True, f"brands={','.join(BRANDS)}" # ---------- util.constants 自身一致性 ---------- def test_constants_required_keys_present() -> tuple[bool, str]: """util.constants 必须含 SKILL_SLUG/SKILL_VERSION/PLATFORM_KIT_MIN_VERSION/BRANDS""" from util import constants missing = [] for key in ("SKILL_SLUG", "SKILL_VERSION", "PLATFORM_KIT_MIN_VERSION", "BRANDS", "CANDIDATE_STATUSES", "REVIEW_CONCLUSIONS", "RISK_DIMENSIONS", "RISK_LEVEL_LABELS"): if not hasattr(constants, key): missing.append(key) if missing: return False, f"util.constants 缺字段: {', '.join(missing)}" return True, "全部必需字段已声明" def test_constants_candidate_statuses_complete() -> tuple[bool, str]: """CANDIDATE_STATUSES 必须覆盖 REQUIREMENTS 0.3 节业务行生命周期的 5 个状态""" from util.constants import CANDIDATE_STATUSES required = ("待审", "已标注", "已复核", "已驳回", "已撤回") missing = [s for s in required if s not in CANDIDATE_STATUSES] if missing: return False, f"CANDIDATE_STATUSES 缺: {', '.join(missing)}" if len(CANDIDATE_STATUSES) != len(required): return False, f"CANDIDATE_STATUSES 多了非预期项: {CANDIDATE_STATUSES}" return True, f"5 个状态齐全: {' / '.join(CANDIDATE_STATUSES)}" def test_constants_review_conclusions_complete() -> tuple[bool, str]: """REVIEW_CONCLUSIONS 必须含保留 / 取消 / 调减""" from util.constants import REVIEW_CONCLUSIONS required = ("保留", "取消", "调减") missing = [s for s in required if s not in REVIEW_CONCLUSIONS] if missing: return False, f"REVIEW_CONCLUSIONS 缺: {', '.join(missing)}" return True, f"{len(REVIEW_CONCLUSIONS)} 个结论齐全" def test_constants_risk_dimensions_complete() -> tuple[bool, str]: """RISK_DIMENSIONS 必须含 REQUIREMENTS 列举的 4 个检测维度""" from util.constants import RISK_DIMENSIONS required = ( "income_vs_card_consumption", "comprehensive_vs_score_rank", "cumulative_aid_amount", "duplicate_aid_history", ) missing = [s for s in required if s not in RISK_DIMENSIONS] if missing: return False, f"RISK_DIMENSIONS 缺: {', '.join(missing)}" return True, f"{len(RISK_DIMENSIONS)} 个检测维度齐全" def test_constants_brands_match_skill_md() -> tuple[bool, str]: """BRANDS 至少含 jiangchang + huashang(v1.0.2 多平台发布声明)""" from util.constants import BRANDS for need in ("jiangchang", "huashang"): if need not in BRANDS: return False, f"BRANDS 缺 {need!r},当前 {BRANDS}" return True, f"BRANDS={list(BRANDS)}" # ---------- main.py CLI 入口 ---------- def test_main_commands_registered() -> tuple[bool, str]: """scripts/main.py COMMANDS 必须注册全部 REQUIREMENTS 0.1 节列出的能力""" from main import COMMANDS required = ( "health", "version", "config-path", "init-db", "verify-candidate", "batch-verify-class", "mark-needs-review", "submit-review-result", "render-html", "sync-card-data", "sync-gdp-data", ) missing = [c for c in required if c not in COMMANDS] if missing: return False, f"COMMANDS 缺: {', '.join(missing)}" return True, f"{len(COMMANDS)} 个命令已注册" def test_main_version_returns_zero() -> tuple[bool, str]: """version() 命令应返回 0""" from main import version buf = io.StringIO() with redirect_stdout(buf): rc = version() if rc != 0: return False, f"version() 返回 {rc}, want 0" if not buf.getvalue().strip(): return False, "version() 输出为空" return True, f"输出={buf.getvalue().strip()!r}" def test_main_version_matches_skill_version() -> tuple[bool, str]: """version() 输出应与 SKILL_VERSION 对齐(避免硬编码漂移)""" from main import version from util.constants import SKILL_VERSION buf = io.StringIO() with redirect_stdout(buf): version() out = buf.getvalue().strip() if SKILL_VERSION not in out: return False, f"version() 输出 {out!r} 不含 SKILL_VERSION={SKILL_VERSION}" return True, f"version() 含 {SKILL_VERSION}" def test_main_health_returns_zero() -> tuple[bool, str]: """health() 应返回 0 且输出非空""" from main import health buf = io.StringIO() with redirect_stdout(buf): rc = health() if rc != 0: return False, f"health() 返回 {rc}, want 0" if not buf.getvalue().strip(): return False, "health() 输出为空" return True, "health() 返回 0 且有输出" def test_main_dispatch_unknown_command_returns_1() -> tuple[bool, str]: """main(['not-a-real-command']) 应返回 1 并打印 usage""" import main as m buf = io.StringIO() with redirect_stdout(buf): rc = m.main(["not-a-real-command"]) if rc != 1: return False, f"main(['not-a-real-command']) = {rc}, want 1" if "Usage" not in buf.getvalue(): return False, f"应打印 usage,实际 {buf.getvalue()!r}" return True, "未知命令 → 1 + Usage" def test_main_dispatch_known_command() -> tuple[bool, str]: """main(['health']) 应路由到 health() 并返回 0""" import main as m buf = io.StringIO() with redirect_stdout(buf): rc = m.main(["health"]) if rc != 0: return False, f"main(['health']) = {rc}, want 0" if "health" not in buf.getvalue().lower(): return False, f"health() 输出应含 'health',实际 {buf.getvalue()!r}" return True, "main(['health']) 路由正确" def test_main_dispatch_no_args_returns_1() -> tuple[bool, str]: """main([]) 应返回 1(无命令)""" import main as m buf = io.StringIO() with redirect_stdout(buf): rc = m.main([]) if rc != 1: return False, f"main([]) = {rc}, want 1" return True, "无命令 → 1" # ---------- 入口 ---------- def main() -> int: parser = argparse.ArgumentParser(description="默认 release 测试套件") parser.add_argument("-v", "--verbose", action="store_true", help="详细输出每个用例结果") args = parser.parse_args() global _VERBOSE _VERBOSE = args.verbose print("=== verify-scholarship-fraud-sas release tests ===", flush=True) _run("constants/skill_md_version_alignment", test_skill_md_version_alignment) _run("constants/skill_md_slug_alignment", test_skill_md_slug_alignment) _run("constants/skill_md_brands_alignment", test_skill_md_brands_alignment) _run("constants/required_keys_present", test_constants_required_keys_present) _run("constants/candidate_statuses_complete", test_constants_candidate_statuses_complete) _run("constants/review_conclusions_complete", test_constants_review_conclusions_complete) _run("constants/risk_dimensions_complete", test_constants_risk_dimensions_complete) _run("constants/brands_match_skill_md", test_constants_brands_match_skill_md) _run("main/commands_registered", test_main_commands_registered) _run("main/version_returns_zero", test_main_version_returns_zero) _run("main/version_matches_skill_version", test_main_version_matches_skill_version) _run("main/health_returns_zero", test_main_health_returns_zero) _run("main/dispatch_unknown_command", test_main_dispatch_unknown_command_returns_1) _run("main/dispatch_known_command", test_main_dispatch_known_command) _run("main/dispatch_no_args", test_main_dispatch_no_args_returns_1) total = len(_RESULTS) passed = sum(1 for _, ok, _ in _RESULTS if ok) failed = total - passed print() print(f"=== Summary: {passed}/{total} passed, {failed} failed ===", flush=True) if failed: print("\nFailures:") for name, ok, msg in _RESULTS: if not ok: print(f" - {name}: {msg}") return 1 return 0 if __name__ == "__main__": raise SystemExit(main())