重新调整模板
This commit is contained in:
@@ -30,8 +30,8 @@
|
||||
|
||||
从本模板复制出新技能仓库后,建议至少逐项确认:
|
||||
|
||||
- [ ] `python tests/run_tests.py -v` 能通过。
|
||||
- [ ] `python scripts/main.py health` 能通过。
|
||||
- [ ] `python tests/run_tests.py -v` 能通过(含 platform-kit 导入与无 vendored core 守护)。
|
||||
- [ ] `python scripts/main.py health` 能通过(输出 runtime diagnostics;warning 不导致非零退出)。
|
||||
- [ ] `python scripts/main.py version` 输出 JSON,且 `skill` 与目录名 / `SKILL.md` / `constants.SKILL_SLUG` 一致。
|
||||
- [ ] 所有 DB / 文件写入都在 `IsolatedDataRoot`(或等价隔离)下测试,不写真实数据目录。
|
||||
- [ ] 外部系统默认使用 mock / `FakeAdapter`,不访问真实 API,不打开真实 RPA。
|
||||
@@ -74,6 +74,11 @@ python tests/run_tests.py test_cli_smoke
|
||||
| ``SKILL.md`` slug 与 ``constants.SKILL_SLUG`` | ``test_skill_metadata.py`` |
|
||||
| DB / ``task_logs`` 骨架冒烟 | ``test_db_smoke.py`` |
|
||||
| **adapter profile / 外呼授权策略** | ``test_adapter_profile_policy.py`` + ``adapter_test_utils.py``(非 ``test_`` 前缀,不自动当用例收集) |
|
||||
| **无 vendored ``jiangchang_skill_core``** | ``test_platform_import.py`` |
|
||||
| **runtime diagnostics 架构守护** | ``test_runtime_diagnostics_integration.py`` |
|
||||
| **文档/runtime 标准守护** | ``test_template_runtime_standard.py`` |
|
||||
|
||||
复制为新技能后**不得删除**上述架构守护测试,除非同步更新模板标准并理解影响。
|
||||
|
||||
### 1.3 数据隔离(``_support.IsolatedDataRoot``)
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@ def get_skill_root() -> str:
|
||||
return _SKILL_ROOT
|
||||
|
||||
|
||||
def platform_kit_version_patch(version: str = "1.0.11"):
|
||||
"""Mock installed jiangchang-platform-kit version for health/diagnostics tests."""
|
||||
from unittest.mock import patch
|
||||
|
||||
return patch(
|
||||
"jiangchang_skill_core.runtime_diagnostics._platform_kit_version",
|
||||
return_value=version,
|
||||
)
|
||||
|
||||
|
||||
class IsolatedDataRoot:
|
||||
"""
|
||||
在独立临时目录下模拟数据根,避免污染本机默认数据目录。
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
import unittest
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
|
||||
from _support import IsolatedDataRoot
|
||||
from _support import IsolatedDataRoot, platform_kit_version_patch
|
||||
|
||||
# scripts/ 已由 _support 注入 sys.path
|
||||
from cli.app import main
|
||||
@@ -27,10 +27,23 @@ class TestCliSmoke(unittest.TestCase):
|
||||
self.assertIn("health", out)
|
||||
|
||||
def test_health_zero(self) -> None:
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
||||
rc = main(["health"])
|
||||
self.assertEqual(rc, 0)
|
||||
old_record = os.environ.get("OPENCLAW_RECORD_VIDEO")
|
||||
os.environ["OPENCLAW_RECORD_VIDEO"] = "0"
|
||||
try:
|
||||
buf = io.StringIO()
|
||||
with platform_kit_version_patch(), redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
||||
rc = main(["health"])
|
||||
self.assertEqual(rc, 0)
|
||||
out = buf.getvalue()
|
||||
self.assertIn("health:", out)
|
||||
self.assertIn("python_executable:", out)
|
||||
self.assertIn("platform_kit_version:", out)
|
||||
self.assertIn("jiangchang_skill_core_file:", out)
|
||||
finally:
|
||||
if old_record is None:
|
||||
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
||||
else:
|
||||
os.environ["OPENCLAW_RECORD_VIDEO"] = old_record
|
||||
|
||||
def test_version_json_and_matches_constants_slug(self) -> None:
|
||||
buf = io.StringIO()
|
||||
|
||||
117
tests/test_platform_import.py
Normal file
117
tests/test_platform_import.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""防回归:公共能力必须从共享 platform-kit 导入,而非技能 vendored 副本。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata as metadata
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from _support import get_scripts_dir, get_skill_root
|
||||
|
||||
|
||||
def _parse_platform_kit_min_version(skill_md_text: str) -> str:
|
||||
parts = skill_md_text.split("---", 2)
|
||||
if len(parts) < 2:
|
||||
raise ValueError("missing frontmatter")
|
||||
m = re.search(
|
||||
r"platform_kit_min_version:\s*[\"']?([^\"'\s]+)[\"']?",
|
||||
parts[1],
|
||||
)
|
||||
if not m:
|
||||
raise ValueError("platform_kit_min_version not found")
|
||||
return m.group(1).strip()
|
||||
|
||||
|
||||
class TestPlatformImportSource(unittest.TestCase):
|
||||
def test_jiangchang_skill_core_not_from_skill_scripts(self) -> None:
|
||||
scripts_dir = os.path.abspath(get_scripts_dir())
|
||||
skill_root = os.path.abspath(get_skill_root())
|
||||
vendored = os.path.join(scripts_dir, "jiangchang_skill_core")
|
||||
|
||||
self.assertFalse(
|
||||
os.path.isdir(vendored),
|
||||
f"vendored package must not exist: {vendored}",
|
||||
)
|
||||
|
||||
import jiangchang_skill_core
|
||||
import jiangchang_skill_core.rpa.video_session as video_session
|
||||
|
||||
core_file = os.path.abspath(jiangchang_skill_core.__file__)
|
||||
video_file = os.path.abspath(video_session.__file__)
|
||||
|
||||
self.assertNotIn(
|
||||
scripts_dir.lower(),
|
||||
core_file.lower(),
|
||||
msg=f"jiangchang_skill_core loaded from skill scripts: {core_file}",
|
||||
)
|
||||
self.assertNotIn(
|
||||
skill_root.lower(),
|
||||
core_file.lower(),
|
||||
msg=f"jiangchang_skill_core loaded from skill tree: {core_file}",
|
||||
)
|
||||
self.assertNotIn(
|
||||
scripts_dir.lower(),
|
||||
video_file.lower(),
|
||||
msg=f"video_session loaded from skill scripts: {video_file}",
|
||||
)
|
||||
|
||||
from jiangchang_skill_core.rpa.video_session import RpaVideoSession
|
||||
|
||||
self.assertTrue(callable(RpaVideoSession))
|
||||
|
||||
def test_skill_tree_load_issue_helper(self) -> None:
|
||||
from jiangchang_skill_core import is_jiangchang_skill_core_from_skill_tree
|
||||
|
||||
skill_root = os.path.abspath(get_skill_root())
|
||||
fake = os.path.join(skill_root, "scripts", "jiangchang_skill_core", "__init__.py")
|
||||
self.assertTrue(
|
||||
is_jiangchang_skill_core_from_skill_tree(skill_root=skill_root, core_file=fake)
|
||||
)
|
||||
self.assertFalse(
|
||||
is_jiangchang_skill_core_from_skill_tree(
|
||||
skill_root=skill_root, core_file=sys.executable
|
||||
)
|
||||
)
|
||||
|
||||
def test_platform_kit_min_version_is_1_0_11(self) -> None:
|
||||
from jiangchang_skill_core import version_ge
|
||||
from util.constants import PLATFORM_KIT_MIN_VERSION
|
||||
|
||||
self.assertEqual(PLATFORM_KIT_MIN_VERSION, "1.0.11")
|
||||
|
||||
md_path = os.path.join(get_skill_root(), "SKILL.md")
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
md = f.read()
|
||||
self.assertEqual(_parse_platform_kit_min_version(md), "1.0.11")
|
||||
|
||||
req_path = os.path.join(get_skill_root(), "requirements.txt")
|
||||
with open(req_path, encoding="utf-8") as f:
|
||||
req = f.read()
|
||||
self.assertIn("jiangchang-platform-kit>=1.0.11", req)
|
||||
|
||||
installed = metadata.version("jiangchang-platform-kit")
|
||||
self.assertTrue(
|
||||
version_ge(installed, PLATFORM_KIT_MIN_VERSION),
|
||||
msg=f"installed {installed!r} < required {PLATFORM_KIT_MIN_VERSION!r}",
|
||||
)
|
||||
|
||||
def test_collect_runtime_diagnostics_importable(self) -> None:
|
||||
from jiangchang_skill_core import collect_runtime_diagnostics
|
||||
|
||||
self.assertTrue(callable(collect_runtime_diagnostics))
|
||||
|
||||
def test_main_import_smoke(self) -> None:
|
||||
scripts_dir = get_scripts_dir()
|
||||
if scripts_dir not in sys.path:
|
||||
sys.path.insert(0, scripts_dir)
|
||||
|
||||
from cli.app import build_parser
|
||||
|
||||
parser = build_parser()
|
||||
self.assertIsNotNone(parser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
72
tests/test_runtime_diagnostics_integration.py
Normal file
72
tests/test_runtime_diagnostics_integration.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Architecture guard: runtime diagnostics must come from platform-kit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from _support import get_scripts_dir, get_skill_root
|
||||
|
||||
_FORBIDDEN_DEFINITIONS = (
|
||||
"class RuntimeDiagnostics",
|
||||
"class RuntimeIssue",
|
||||
"def collect_runtime_diagnostics",
|
||||
"def format_runtime_health_lines",
|
||||
"def runtime_diagnostics_dict",
|
||||
"def _is_git_lfs_pointer",
|
||||
"def _is_usable_audio_file",
|
||||
"def _probe_background_music",
|
||||
)
|
||||
|
||||
|
||||
class TestRuntimeDiagnosticsIntegration(unittest.TestCase):
|
||||
def test_no_vendored_jiangchang_skill_core(self) -> None:
|
||||
vendored = os.path.join(get_scripts_dir(), "jiangchang_skill_core")
|
||||
self.assertFalse(os.path.isdir(vendored), vendored)
|
||||
|
||||
def test_runtime_diagnostics_module_removed(self) -> None:
|
||||
legacy = os.path.join(get_scripts_dir(), "util", "runtime_diagnostics.py")
|
||||
self.assertFalse(os.path.isfile(legacy), legacy)
|
||||
|
||||
def test_util_py_files_do_not_reimplement_runtime_diagnostics(self) -> None:
|
||||
util_dir = os.path.join(get_scripts_dir(), "util")
|
||||
for name in os.listdir(util_dir):
|
||||
if not name.endswith(".py"):
|
||||
continue
|
||||
path = os.path.join(util_dir, name)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
for forbidden in _FORBIDDEN_DEFINITIONS:
|
||||
self.assertNotIn(
|
||||
forbidden,
|
||||
text,
|
||||
msg=f"{name} must not define {forbidden}",
|
||||
)
|
||||
|
||||
def test_task_service_imports_platform_kit_diagnostics(self) -> None:
|
||||
path = os.path.join(get_scripts_dir(), "service", "task_service.py")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
self.assertIn("from jiangchang_skill_core import", text)
|
||||
self.assertIn("collect_runtime_diagnostics", text)
|
||||
self.assertIn("format_runtime_health_lines", text)
|
||||
self.assertIn("PLATFORM_KIT_MIN_VERSION", text)
|
||||
self.assertNotIn("from util.runtime_diagnostics import", text)
|
||||
|
||||
def test_collect_runtime_diagnostics_callable_from_platform_kit(self) -> None:
|
||||
from jiangchang_skill_core import collect_runtime_diagnostics
|
||||
|
||||
from util.constants import PLATFORM_KIT_MIN_VERSION, SKILL_SLUG
|
||||
from util.runtime_paths import get_skill_root
|
||||
|
||||
diag = collect_runtime_diagnostics(
|
||||
skill_slug=SKILL_SLUG,
|
||||
platform_kit_min_version=PLATFORM_KIT_MIN_VERSION,
|
||||
skill_root=get_skill_root(),
|
||||
)
|
||||
self.assertEqual(diag.skill_slug, SKILL_SLUG)
|
||||
self.assertEqual(diag.platform_kit_min_version, PLATFORM_KIT_MIN_VERSION)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
77
tests/test_template_runtime_standard.py
Normal file
77
tests/test_template_runtime_standard.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Docs guard: template must promote shared runtime, not vendored jiangchang_skill_core."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from _support import get_skill_root
|
||||
|
||||
DOC_PATHS = (
|
||||
"README.md",
|
||||
"SKILL.md",
|
||||
"references/RUNTIME.md",
|
||||
"references/CLI.md",
|
||||
"references/DEVELOPMENT.md",
|
||||
"references/RPA.md",
|
||||
"references/CONFIG.md",
|
||||
)
|
||||
|
||||
FORBIDDEN_PHRASES = (
|
||||
"scripts/jiangchang_skill_core/:运行时",
|
||||
"运行时与统一日志副本",
|
||||
"vendor 源码在 scripts/jiangchang_skill_core",
|
||||
"模板通过 vendor 拷贝引用",
|
||||
"vendor 自 platform-kit",
|
||||
"模板 vendor 拷贝在 scripts/jiangchang_skill_core",
|
||||
)
|
||||
|
||||
POSITIVE_MARKERS = (
|
||||
"jiangchang-platform-kit",
|
||||
"1.0.11",
|
||||
"共享 runtime",
|
||||
)
|
||||
|
||||
|
||||
class TestTemplateRuntimeStandard(unittest.TestCase):
|
||||
def _read(self, rel_path: str) -> str:
|
||||
path = os.path.join(get_skill_root(), rel_path)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def test_guarded_docs_avoid_vendored_core_guidance(self) -> None:
|
||||
for rel_path in DOC_PATHS:
|
||||
text = self._read(rel_path)
|
||||
for phrase in FORBIDDEN_PHRASES:
|
||||
self.assertNotIn(
|
||||
phrase,
|
||||
text,
|
||||
msg=f"{rel_path} must not contain {phrase!r}",
|
||||
)
|
||||
|
||||
def test_guarded_docs_promote_shared_runtime_standard(self) -> None:
|
||||
combined = "\n".join(self._read(p) for p in DOC_PATHS)
|
||||
for marker in POSITIVE_MARKERS:
|
||||
self.assertIn(
|
||||
marker,
|
||||
combined,
|
||||
msg=f"docs should mention {marker!r}",
|
||||
)
|
||||
self.assertTrue(
|
||||
"不得 vendored" in combined or "不要 vendor" in combined,
|
||||
msg="docs should state skills must not vendor jiangchang_skill_core",
|
||||
)
|
||||
|
||||
def test_runtime_md_mentions_collect_runtime_diagnostics(self) -> None:
|
||||
text = self._read("references/RUNTIME.md")
|
||||
self.assertIn("collect_runtime_diagnostics", text)
|
||||
|
||||
def test_cli_md_mentions_health_runtime_diagnostics(self) -> None:
|
||||
text = self._read("references/CLI.md")
|
||||
self.assertIn("python_executable", text)
|
||||
self.assertIn("platform_kit_version", text)
|
||||
self.assertIn("jiangchang_skill_core_file", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user