chore: auto release commit (2026-09-14 16:39:09)

This commit is contained in:
2026-09-14 16:39:09 +08:00
parent 3f87f223f9
commit 298e41c2c6
5 changed files with 371 additions and 2 deletions

View File

@@ -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 派生

View File

@@ -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
---

View File

@@ -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

34
scripts/util/constants.py Normal file
View File

@@ -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")

323
tests/run_tests.py Normal file
View File

@@ -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<list>(?:\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())