@@ -0,0 +1,360 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" 默认测试套件:课程成绩异常诊断与报告生成。
设计原则:
- 仅用标准库(不依赖 pytest / jinja2) , CI 与本地均能直接 `python tests/run_tests.py` 跑
- v1.0.1 是骨架版(仅 main.py 占位 + 空 __init__.py) , v1.0.2 同步发布声明,本套件覆盖:
- util.constants 版本对齐( POLICY-RELEASE-001)
- SKILL.md / CHANGELOG.md 顶部 version / slug 一致性
- scripts/main.py CLI 路由表的命令完整性
- scripts/util/constants.py 阈值与等级常量自洽
- 退出码: 0 = 全部通过, 1 = 有失败
- -v 开关:详细输出每个用例的状态
release.ps1 在打 tag 前会自动跑 `python tests/run_tests.py -v`,任何一个
test_* 返回 (False, msg) 即 abort 发布。
"""
from __future__ import annotations
import argparse
import re
import sys
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_*,记录结果。 """
import traceback
try :
ok , msg = fn ( )
except Exception as e :
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 } "
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_declaration ( ) - > tuple [ bool , str ] :
""" v1.0.2+ 要求 metadata.openclaw.brands 含 jiangchang + huashang """
skill_md = ( _REPO_ROOT / " SKILL.md " ) . read_text ( encoding = " utf-8 " )
# 定位 metadata.openclaw 块(顶层 key 之前都属 metadata 范围)
# 简单做法:先找 brands: 行,再往后扫 10 行,仅收录缩进 > 0 且以 - 开头的项
m = re . search ( r ' (?m)^[ \ t]*brands:.*$ ' , skill_md )
if not m :
return False , " SKILL.md metadata.openclaw 缺 brands: 声明 "
after = skill_md [ m . end ( ) : ] . splitlines ( )
# brands 列表项最长 2~3 行( jiangchang + huashang + 注释),扫 8 行足够
brands : set [ str ] = set ( )
for line in after [ : 8 ] :
# 列表项必须以 2+ 个空格 + - + 空格开头
item = re . match ( r ' ^[ \ t] { 2,}- \ s+([a-z0-9_-]+) \ s*(?:#.*)?$ ' , line )
if item :
brands . add ( item . group ( 1 ) )
continue
# 遇下一个非缩进/非 - 项则停止 brands 块
if line . strip ( ) and not line . startswith ( " " ) and not line . startswith ( " \t " ) :
break
if " jiangchang " not in brands :
return False , f " brands 缺 jiangchang, 实际 { sorted ( brands ) } "
if " huashang " not in brands :
return False , f " brands 缺 huashang, 实际 { sorted ( brands ) } "
return True , f " brands= { sorted ( brands ) } "
def test_changelog_has_v102_entry ( ) - > tuple [ bool , str ] :
""" CHANGELOG.md 含 v1.0.2 条目 """
from util . constants import SKILL_VERSION
cl = ( _REPO_ROOT / " CHANGELOG.md " ) . read_text ( encoding = " utf-8 " )
if f " ## { SKILL_VERSION } " not in cl :
return False , f " CHANGELOG 缺 ## { SKILL_VERSION } 段 "
return True , f " ## { SKILL_VERSION } 条目已存在 "
# ---------- util.constants 阈值与等级自洽 ----------
def test_constants_anomaly_patterns ( ) - > tuple [ bool , str ] :
""" ANOMALY_PATTERNS 含三种核心异常模式 """
from util . constants import ANOMALY_PATTERNS
expected = { " 高分低能 " , " 两极分化 " , " 挂科异常 " }
got = set ( ANOMALY_PATTERNS )
if got != expected :
return False , f " ANOMALY_PATTERNS= { sorted ( got ) } , want { sorted ( expected ) } "
return True , " 3 种异常模式齐全 "
def test_constants_alert_levels_order ( ) - > tuple [ bool , str ] :
""" ALERT_LEVELS 与 ALERT_LEVEL_LABELS 顺序一一对应 """
from util . constants import ALERT_LEVELS , ALERT_LEVEL_LABELS
if tuple ( ALERT_LEVEL_LABELS . keys ( ) ) != ALERT_LEVELS :
return False , (
f " ALERT_LEVEL_LABELS 键= { tuple ( ALERT_LEVEL_LABELS . keys ( ) ) } "
f " != ALERT_LEVELS= { ALERT_LEVELS } "
)
for lv in ALERT_LEVELS :
if not ALERT_LEVEL_LABELS [ lv ] :
return False , f " 等级 { lv } 标签为空 "
return True , f " 3 个等级 (yellow/orange/red) 标签齐全 "
def test_constants_thresholds_positive ( ) - > tuple [ bool , str ] :
""" 核心阈值必须为正数,且大小关系合理 """
from util . constants import (
HIGH_SCORE_THRESHOLD ,
LOW_SCORE_THRESHOLD ,
HIGH_FAIL_RATE_THRESHOLD ,
POLARIZATION_SPREAD_THRESHOLD ,
)
if not ( 0 < HIGH_SCORE_THRESHOLD < = 100 ) :
return False , f " HIGH_SCORE_THRESHOLD= { HIGH_SCORE_THRESHOLD } , 应在 (0,100] "
if not ( 0 < LOW_SCORE_THRESHOLD < = 100 ) :
return False , f " LOW_SCORE_THRESHOLD= { LOW_SCORE_THRESHOLD } , 应在 (0,100] "
if LOW_SCORE_THRESHOLD > = HIGH_SCORE_THRESHOLD :
return False , f " 低分阈值 { LOW_SCORE_THRESHOLD } 应 < 高分阈值 { HIGH_SCORE_THRESHOLD } "
if not ( 0 < HIGH_FAIL_RATE_THRESHOLD < 1 ) :
return False , f " HIGH_FAIL_RATE_THRESHOLD= { HIGH_FAIL_RATE_THRESHOLD } , 应在 (0,1) "
if POLARIZATION_SPREAD_THRESHOLD < = 0 :
return False , f " POLARIZATION_SPREAD_THRESHOLD= { POLARIZATION_SPREAD_THRESHOLD } , 应 > 0 "
return True , " 4 个阈值合规 "
def test_constants_platform_kit_min_version ( ) - > tuple [ bool , str ] :
""" PLATFORM_KIT_MIN_VERSION 必须形如 X.Y.Z """
from util . constants import PLATFORM_KIT_MIN_VERSION
if not re . match ( r ' ^ \ d+ \ . \ d+ \ . \ d+$ ' , PLATFORM_KIT_MIN_VERSION ) :
return False , f " PLATFORM_KIT_MIN_VERSION= { PLATFORM_KIT_MIN_VERSION } 不是 X.Y.Z "
return True , f " PLATFORM_KIT_MIN_VERSION= { PLATFORM_KIT_MIN_VERSION } "
def test_constants_high_risk_labels_consistent ( ) - > tuple [ bool , str ] :
""" HIGH_RISK_LABELS 同时含 True / False 两个键 """
from util . constants import HIGH_RISK_LABELS
if True not in HIGH_RISK_LABELS or False not in HIGH_RISK_LABELS :
return False , f " HIGH_RISK_LABELS 应含 True/False 两个键,实测 { list ( HIGH_RISK_LABELS . keys ( ) ) } "
return True , " True/False 双键齐全 "
# ---------- 核心 repository / 业务骨架自检 ----------
def test_main_py_commands_complete ( ) - > tuple [ bool , str ] :
""" scripts/main.py COMMANDS 含 9 个核心命令 """
expected = {
" health " , " version " , " config-path " , " init-db " ,
" analyze-grade-distribution " , " batch-analyze-courses " ,
" render-html " , " flag-high-risk-course " , " sync-historical-grades " ,
}
import importlib . util
spec = importlib . util . spec_from_file_location ( " _main " , _SCRIPTS_DIR / " main.py " )
assert spec and spec . loader
mod = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( mod )
got = set ( mod . COMMANDS . keys ( ) )
missing = expected - got
extra = got - expected
if missing or extra :
return False , f " missing= { sorted ( missing ) } extra= { sorted ( extra ) } "
return True , f " COMMANDS 含全部 { len ( expected ) } 个命令 "
def test_main_py_version_command_prints_version ( ) - > tuple [ bool , str ] :
""" version 命令打印的版本号必须 == SKILL_VERSION """
from util . constants import SKILL_VERSION
import io
import contextlib
import importlib . util
spec = importlib . util . spec_from_file_location ( " _main " , _SCRIPTS_DIR / " main.py " )
assert spec and spec . loader
mod = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( mod )
buf = io . StringIO ( )
with contextlib . redirect_stdout ( buf ) :
rc = mod . version ( )
out = buf . getvalue ( )
if rc != 0 :
return False , f " version() 返回 { rc } , want 0 "
if SKILL_VERSION not in out :
return False , f " version 输出不含 { SKILL_VERSION } ,实际 { out !r} "
return True , f " version 命令打印含 { SKILL_VERSION } "
def test_main_py_analyze_command_does_not_crash ( ) - > tuple [ bool , str ] :
""" analyze-grade-distribution 接受 course_id 占位,不抛异常 """
import importlib . util
spec = importlib . util . spec_from_file_location ( " _main " , _SCRIPTS_DIR / " main.py " )
assert spec and spec . loader
mod = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( mod )
rc = mod . analyze_grade_distribution ( " COURSE-TEST-001 " )
if rc != 0 :
return False , f " analyze_grade_distribution 返回 { rc } , want 0 "
return True , " analyze-grade-distribution 占位命令可调用 "
def test_main_py_main_dispatch ( ) - > tuple [ bool , str ] :
""" main(argv) 能正确分发到 version 命令 """
import importlib . util
spec = importlib . util . spec_from_file_location ( " _main " , _SCRIPTS_DIR / " main.py " )
assert spec and spec . loader
mod = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( mod )
rc = mod . main ( [ " version " ] )
if rc != 0 :
return False , f " main([ ' version ' ]) 返回 { rc } , want 0 "
rc = mod . main ( [ " __nope__ " ] )
if rc != 1 :
return False , f " main([ ' __nope__ ' ]) 返回 { rc } , want 1( 未知命令) "
return True , " argv 分发 OK( version=0 / 未知命令=1) "
# ---------- 服务层 / 异常识别占位自检 ----------
def test_anomaly_classification_thresholds_consistent_with_constants ( ) - > tuple [ bool , str ] :
""" 若未来引入 service 模块,分类阈值必须与 constants 一致。
v1.0.2 仍为骨架,本测试仅校验 constants 中阈值即可,作为常量化的契约保护。
"""
from util . constants import (
HIGH_SCORE_THRESHOLD ,
LOW_SCORE_THRESHOLD ,
HIGH_FAIL_RATE_THRESHOLD ,
POLARIZATION_SPREAD_THRESHOLD ,
)
# 高分线必须显著高于低分线(差距 ≥ 10 分)才有「高分低能」识别空间
if HIGH_SCORE_THRESHOLD - LOW_SCORE_THRESHOLD < 10 :
return False , (
f " 高分线 { HIGH_SCORE_THRESHOLD } 与低分线 { LOW_SCORE_THRESHOLD } 差距过小, "
f " 无法区分「高分低能」与「两极分化」 "
)
# 挂科率阈值 ≥ 0.1 才有意义
if HIGH_FAIL_RATE_THRESHOLD < 0.1 :
return False , f " 挂科率阈值 { HIGH_FAIL_RATE_THRESHOLD } 过低,噪声大 "
# 两极分化极差阈值 ≥ 20 分
if POLARIZATION_SPREAD_THRESHOLD < 20 :
return False , f " 两极分化极差阈值 { POLARIZATION_SPREAD_THRESHOLD } 过小 "
return True , " 4 个阈值满足业务识别能力 "
def test_scripts_subdirs_present ( ) - > tuple [ bool , str ] :
""" scripts/ { cli,db,service,util} 四个分层目录必须存在 """
for sub in ( " cli " , " db " , " service " , " util " ) :
p = _SCRIPTS_DIR / sub
if not p . is_dir ( ) :
return False , f " scripts/ { sub } 目录不存在 "
return True , " cli/db/service/util 四个分层齐全 "
# ---------- 入口 ----------
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 ( " === analyze-grade-distribution-aa 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_declaration " , test_skill_md_brands_declaration )
_run ( " constants/changelog_v102_entry " , test_changelog_has_v102_entry )
_run ( " constants/anomaly_patterns " , test_constants_anomaly_patterns )
_run ( " constants/alert_levels_order " , test_constants_alert_levels_order )
_run ( " constants/thresholds_positive " , test_constants_thresholds_positive )
_run ( " constants/platform_kit_min_version " , test_constants_platform_kit_min_version )
_run ( " constants/high_risk_labels_consistent " , test_constants_high_risk_labels_consistent )
_run ( " scripts/subdirs_present " , test_scripts_subdirs_present )
_run ( " main/commands_complete " , test_main_py_commands_complete )
_run ( " main/version_prints_skill_version " , test_main_py_version_command_prints_version )
_run ( " main/analyze_no_crash " , test_main_py_analyze_command_does_not_crash )
_run ( " main/argv_dispatch " , test_main_py_main_dispatch )
_run ( " domain/anomaly_thresholds_consistent " , test_anomaly_classification_thresholds_consistent_with_constants )
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 ( " \n Failures: " )
for name , ok , msg in _RESULTS :
if not ok :
print ( f " - { name } : { msg } " )
return 1
return 0
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )