chore: auto release commit (2026-06-08 14:19:45)
All checks were successful
技能自动化发布 / release (push) Successful in 6s

This commit is contained in:
2026-06-08 14:19:46 +08:00
parent 5b555e4313
commit a24f6809da
23 changed files with 701 additions and 184 deletions

View File

@@ -39,7 +39,7 @@ def get_skill_root() -> str:
return _SKILL_ROOT
def platform_kit_version_patch(version: str = "1.0.11"):
def platform_kit_version_patch(version: str = "1.0.13"):
"""Mock installed jiangchang-platform-kit version for health/diagnostics tests."""
from unittest.mock import patch

View File

@@ -10,6 +10,8 @@ from contextlib import redirect_stderr, redirect_stdout
from _support import IsolatedDataRoot, platform_kit_version_patch
from jiangchang_skill_core import config
# scripts/ 已由 _support 注入 sys.path
from cli.app import main
from util.constants import SKILL_SLUG
@@ -71,17 +73,25 @@ class TestCliSmoke(unittest.TestCase):
self.assertIn("数字", buf.getvalue())
def test_run_placeholder_returns_nonzero_without_network(self) -> None:
"""占位 run不验证业务成功,仅确认模板提示且退出码非 0无 AUTH_BASE 时不发 HTTP"""
"""占位 runRpaVideoSession 示范 + 模板提示OPENCLAW_RECORD_VIDEO=0 时不启 ffmpeg"""
old_auth = os.environ.pop("JIANGCHANG_AUTH_BASE_URL", None)
old_record = os.environ.get("OPENCLAW_RECORD_VIDEO")
os.environ["OPENCLAW_RECORD_VIDEO"] = "0"
try:
buf = io.StringIO()
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
rc = main(["run"])
self.assertNotEqual(rc, 0)
self.assertIn("模板", buf.getvalue())
with IsolatedDataRoot(user_id="_cli_run"):
config.reset_cache()
buf = io.StringIO()
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
rc = main(["run"])
self.assertNotEqual(rc, 0)
self.assertIn("模板", buf.getvalue())
finally:
if old_auth is not None:
os.environ["JIANGCHANG_AUTH_BASE_URL"] = old_auth
if old_record is None:
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
else:
os.environ["OPENCLAW_RECORD_VIDEO"] = old_record
if __name__ == "__main__":

View File

@@ -78,8 +78,8 @@ class TestConfigBootstrap(unittest.TestCase):
config.reset_cache()
with open(example, encoding="utf-8") as f:
example_text = f.read()
self.assertIn("OPENCLAW_TEST_TARGET=simulator_rpa", example_text)
self.assertEqual(config.get("OPENCLAW_TEST_TARGET"), "simulator_rpa")
self.assertIn("OPENCLAW_TEST_TARGET=mock", example_text)
self.assertEqual(config.get("OPENCLAW_TEST_TARGET"), "mock")
def test_config_path_outputs_json(self) -> None:
with IsolatedDataRoot(user_id="_cfg_path"):

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
"""文档与 pytest 配置守护:防止模板标准退化。"""
from __future__ import annotations
import configparser
import os
import unittest
from _support import get_skill_root
class TestDocsStandards(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_rpa_md_covers_video_and_playwright_standards(self) -> None:
text = self._read("references/RPA.md")
self.assertIn("title", text)
self.assertIn("closing_title", text)
self.assertIn("中文", text)
self.assertIn("--no-sandbox", text)
self.assertIn("--disable-blink-features=AutomationControlled", text)
self.assertIn("RpaVideoSession", text)
self.assertIn("1.0.13", text)
def test_adapter_md_covers_four_tiers_and_allow_flags(self) -> None:
text = self._read("references/ADAPTER.md")
for marker in (
"mock",
"simulator_rpa",
"real_api",
"real_rpa",
"ALLOW_REAL_API",
"ALLOW_REAL_RPA",
"ALLOW_WRITE_ACTIONS",
"sibling_bridge",
):
self.assertIn(marker, text, msg=f"ADAPTER.md missing {marker!r}")
def test_cli_md_mentions_shared_python_runtime(self) -> None:
text = self._read("references/CLI.md")
self.assertIn("python-runtime", text)
self.assertIn("uv run python", text)
def test_testing_md_mentions_pytest_txt_collection_guard(self) -> None:
text = self._read("references/TESTING.md")
self.assertIn(".txt", text)
self.assertIn("pytest.ini", text)
def test_docs_do_not_claim_1_0_11_as_current_standard(self) -> None:
for rel in (
"README.md",
"SKILL.md",
"references/RUNTIME.md",
"references/RPA.md",
"references/CONFIG.md",
"references/TESTING.md",
):
text = self._read(rel)
self.assertNotIn(
">=1.0.11",
text.replace(" ", ""),
msg=f"{rel} still claims 1.0.11 as current minimum",
)
class TestPytestIniCollection(unittest.TestCase):
def test_pytest_ini_exists_and_limits_python_files(self) -> None:
ini_path = os.path.join(get_skill_root(), "pytest.ini")
self.assertTrue(os.path.isfile(ini_path), "pytest.ini must exist at repo root")
parser = configparser.ConfigParser()
parser.read(ini_path, encoding="utf-8")
python_files = parser.get("pytest", "python_files", fallback="")
self.assertIn("test_*.py", python_files)
self.assertIn("*_test.py", python_files)
if __name__ == "__main__":
unittest.main()

View File

@@ -10,6 +10,19 @@ import unittest
from _support import IsolatedDataRoot, get_skill_root
import importlib.metadata as metadata
from jiangchang_skill_core import version_ge
from util.constants import PLATFORM_KIT_MIN_VERSION
def _platform_kit_meets_template_minimum() -> bool:
try:
installed = metadata.version("jiangchang-platform-kit")
except metadata.PackageNotFoundError:
return False
return version_ge(installed, PLATFORM_KIT_MIN_VERSION)
def _run_main(args: list[str], timeout: int = 15) -> subprocess.CompletedProcess[str]:
root = get_skill_root()
@@ -30,6 +43,10 @@ def _run_main(args: list[str], timeout: int = 15) -> subprocess.CompletedProcess
class TestEntrypointSubprocess(unittest.TestCase):
@unittest.skipUnless(
_platform_kit_meets_template_minimum(),
f"installed jiangchang-platform-kit below template minimum {PLATFORM_KIT_MIN_VERSION}",
)
def test_health_exit_zero(self) -> None:
with IsolatedDataRoot():
cp = _run_main(["health"])

View File

@@ -83,16 +83,16 @@ class TestPlatformImportSource(unittest.TestCase):
)
)
def test_platform_kit_min_version_is_1_0_11(self) -> None:
def test_platform_kit_min_version_is_1_0_13(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")
self.assertEqual(PLATFORM_KIT_MIN_VERSION, "1.0.13")
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")
self.assertEqual(_parse_platform_kit_min_version(md), "1.0.13")
req_path = os.path.join(get_skill_root(), "requirements.txt")
with open(req_path, encoding="utf-8") as f:
@@ -102,7 +102,15 @@ class TestPlatformImportSource(unittest.TestCase):
self.assertNotIn("jiangchang-platform-kit", dep_body)
self.assertNotIn("playwright", dep_body)
installed = metadata.version("jiangchang-platform-kit")
try:
installed = metadata.version("jiangchang-platform-kit")
except metadata.PackageNotFoundError:
self.skipTest("jiangchang-platform-kit not installed in current interpreter")
if not version_ge(installed, PLATFORM_KIT_MIN_VERSION):
self.skipTest(
f"installed jiangchang-platform-kit {installed!r} < required "
f"{PLATFORM_KIT_MIN_VERSION!r}; upgrade shared runtime to enforce this check"
)
self.assertTrue(
version_ge(installed, PLATFORM_KIT_MIN_VERSION),
msg=f"installed {installed!r} < required {PLATFORM_KIT_MIN_VERSION!r}",

View File

@@ -32,7 +32,7 @@ FORBIDDEN_PHRASES = (
POSITIVE_MARKERS = (
"jiangchang-platform-kit",
"1.0.11",
"1.0.13",
"共享 runtime",
)
@@ -75,6 +75,7 @@ class TestTemplateRuntimeStandard(unittest.TestCase):
self.assertIn("python_executable", text)
self.assertIn("platform_kit_version", text)
self.assertIn("jiangchang_skill_core_file", text)
self.assertIn("python-runtime", text)
if __name__ == "__main__":

161
tests/test_video_service.py Normal file
View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
"""RpaVideoSession 模板示范与 result_summary / CLI 输出测试。"""
from __future__ import annotations
import io
import json
import os
import re
import unittest
from contextlib import redirect_stderr, redirect_stdout
from unittest.mock import AsyncMock, MagicMock, patch
from _support import IsolatedDataRoot, get_skill_root
from jiangchang_skill_core import config
class TestEnvExampleVideoDefaults(unittest.TestCase):
def test_env_example_contains_required_keys(self) -> None:
path = os.path.join(get_skill_root(), ".env.example")
with open(path, encoding="utf-8") as f:
text = f.read()
for key in (
"OPENCLAW_TEST_TARGET=mock",
"OPENCLAW_RECORD_VIDEO=1",
"OPENCLAW_ARTIFACTS_ON_FAILURE=1",
"OPENCLAW_BROWSER_HEADLESS=0",
"OPENCLAW_PLAYWRIGHT_STEALTH=1",
):
self.assertIn(key, text, msg=f"missing {key!r} in .env.example")
class TestPrintVideoSummary(unittest.TestCase):
def test_cli_prints_video_path_when_enabled(self) -> None:
from service.task_run_support import _print_video_summary
buf = io.StringIO()
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
_print_video_summary(
{
"enabled": True,
"path": r"D:\data\videos\your-skill-slug_demo.mp4",
"record_log_path": r"D:\data\logs\ffmpeg-record.log",
"warnings": [],
}
)
out = buf.getvalue()
self.assertIn("录屏路径", out)
self.assertIn("your-skill-slug_demo.mp4", out)
self.assertIn("录屏日志", out)
def test_disabled_video_prints_nothing(self) -> None:
from service.task_run_support import _print_video_summary
buf = io.StringIO()
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
_print_video_summary({"enabled": False, "path": r"D:\tmp\x.mp4"})
self.assertEqual(buf.getvalue(), "")
class TestMergeVideoIntoResultSummary(unittest.TestCase):
def test_result_summary_includes_video_fields(self) -> None:
from service.task_run_support import merge_video_into_result_summary
video_info = {
"enabled": True,
"path": r"D:\data\videos\demo.mp4",
"capture_path": r"D:\data\capture.mp4",
"record_log_path": r"D:\data\logs\ffmpeg-record.log",
"warnings": ["test_warning"],
"voiceover_path": r"D:\data\voiceover.wav",
"music_path": r"D:\data\music.mp3",
"audio_warnings": ["tts_skipped"],
}
payload = merge_video_into_result_summary({"demo": True}, video_info)
self.assertIn("video", payload)
self.assertEqual(payload["video_path"], video_info["path"])
self.assertEqual(payload["raw_video"], video_info["capture_path"])
self.assertEqual(payload["video_log"], video_info["record_log_path"])
self.assertEqual(payload["video_warnings"], ["test_warning"])
self.assertEqual(payload["voiceover_path"], video_info["voiceover_path"])
self.assertEqual(payload["music_path"], video_info["music_path"])
self.assertEqual(payload["audio_warnings"], ["tts_skipped"])
class TestTemplateRunVideoSession(unittest.TestCase):
def _fake_video_session(self, mock_video: MagicMock):
class _FakeVideoSession:
def __init__(self, **kwargs):
self.kwargs = kwargs
async def __aenter__(self):
return mock_video
async def __aexit__(self, *args):
return None
return _FakeVideoSession
def test_cmd_run_creates_video_session_with_chinese_titles(self) -> None:
with IsolatedDataRoot(user_id="_video_run"):
os.environ["OPENCLAW_RECORD_VIDEO"] = "0"
config.reset_cache()
from service import task_service
mock_video = MagicMock()
mock_video.add_step = MagicMock()
mock_video.summary.return_value = {
"enabled": False,
"path": None,
"capture_path": None,
"record_log_path": None,
"warnings": [],
"voiceover_path": None,
"music_path": None,
"audio_warnings": [],
}
created: list[dict] = []
class _CapturingFakeSession:
def __init__(self, **kwargs):
created.append(kwargs)
async def __aenter__(self):
return mock_video
async def __aexit__(self, *args):
return None
with patch.object(task_service, "RpaVideoSession", _CapturingFakeSession):
buf = io.StringIO()
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
rc = task_service.cmd_run(target="demo-target", input_id="42")
self.assertEqual(rc, 1)
self.assertTrue(created, "RpaVideoSession should be constructed")
kwargs = created[0]
self.assertEqual(kwargs.get("title"), "开始执行示例任务")
self.assertEqual(kwargs.get("closing_title"), "示例任务执行完成")
self.assertTrue(re.search(r"[\u4e00-\u9fff]", kwargs.get("title", "")))
self.assertTrue(re.search(r"[\u4e00-\u9fff]", kwargs.get("closing_title", "")))
mock_video.add_step.assert_any_call("准备执行示例任务")
mock_video.add_step.assert_any_call("示例任务执行完成")
mock_video.summary.assert_called()
out = buf.getvalue()
self.assertIn("模板", out)
from db import task_logs_repository as tlr
rows = tlr.list_task_logs(1)
self.assertTrue(rows)
summary = json.loads(rows[0][7])
self.assertIn("video", summary)
self.assertIn("video_path", summary)
if __name__ == "__main__":
unittest.main()