From 298e41c2c6874456ad577c82c129c5ed833fb950 Mon Sep 17 00:00:00 2001 From: taojianmin Date: Mon, 14 Sep 2026 16:39:09 +0800 Subject: [PATCH] chore: auto release commit (2026-09-14 16:39:09) --- CHANGELOG.md | 4 + SKILL.md | 5 +- scripts/main.py | 7 +- scripts/util/constants.py | 34 ++++ tests/run_tests.py | 323 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 scripts/util/constants.py create mode 100644 tests/run_tests.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e738a67..20dfd26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ 本文件对应技能市场 **「更新日志」** Tab。 +## 1.0.2 + +- **多平台发布声明**:SKILL.md `metadata.openclaw.brands` 新增 `jiangchang` + `huashang`,发布后同步挂匠厂主站与华商 OEM 市场。CI 会幂等补写 `skill_market_relations` 两行(**只补不删**)。 + ## 1.0.1 - 初版:基于匠厂 skill-template 主仓 v1.0.108 派生 diff --git a/SKILL.md b/SKILL.md index 38cb204..42950e4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 奖助学金评定逻辑防伪校验 description: "辅导员在汇总表中批量勾选候选人后,自动比对自报家庭年收入与一卡通消费、综测排名与成绩排名、本学年累计资助金额,生成《奖助评定复核建议清单》。" -version: 1.0.1 +version: 1.0.2 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -12,6 +12,9 @@ metadata: developer_ids: - 12345 # 占位,请换成你的匠厂用户 ID - 12468 + brands: # 可选;未写则发布后仅挂匠厂市场(jiangchang) + - jiangchang # 匠厂主站 + - huashang # 华商等 OEM:按目标市场填写,可多选 allowed-tools: - bash --- diff --git a/scripts/main.py b/scripts/main.py index 59aa966..6c1f7e1 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -19,7 +19,12 @@ def health(): def version(): """查看版本(占位)。""" - print("verify-scholarship-fraud-sas v1.0.1") + try: + from util.constants import SKILL_VERSION + ver = SKILL_VERSION + except ImportError: + ver = "1.0.1" # 占位回退 + print(f"verify-scholarship-fraud-sas v{ver}") return 0 diff --git a/scripts/util/constants.py b/scripts/util/constants.py new file mode 100644 index 0000000..d136751 --- /dev/null +++ b/scripts/util/constants.py @@ -0,0 +1,34 @@ +"""技能标识、版本与阈值常量。 + +复制到新 skill 时必须修改: +- SKILL_SLUG 与 SKILL.md metadata.openclaw.slug 保持一致 +- SKILL_VERSION 与 SKILL.md version 保持一致 +""" + +SKILL_SLUG = "verify-scholarship-fraud-sas" +SKILL_VERSION = "1.0.2" +LOG_LOGGER_NAME = "openclaw.skill.verify_scholarship_fraud_sas" +PLATFORM_KIT_MIN_VERSION = "1.2.11" + +# 业务行生命周期状态值(候选人汇总表每一行) +CANDIDATE_STATUSES = ("待审", "已标注", "已复核", "已驳回", "已撤回") + +# 复核结论 +REVIEW_CONCLUSIONS = ("保留", "取消", "调减") + +# 异常检测维度(v1.0.1 占位;具体阈值由开发者按 development/REQUIREMENTS.md 补全) +RISK_DIMENSIONS = ( + "income_vs_card_consumption", # 自报年收入 vs 一卡通月均消费 + "comprehensive_vs_score_rank", # 综测排名 vs 成绩排名 + "cumulative_aid_amount", # 本学年累计资助金额 + "duplicate_aid_history", # 历史重复资助排查 +) + +RISK_LEVEL_LABELS = { + "low": "低风险", + "medium": "中风险", + "high": "高风险", +} + +# 多平台 brands 同步声明(与 SKILL.md metadata.openclaw.brands 保持一致) +BRANDS = ("jiangchang", "huashang") \ No newline at end of file diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..6650c48 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,323 @@ +#!/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()) \ No newline at end of file