chore: auto release commit (2026-06-08 14:19:45)
All checks were successful
技能自动化发布 / release (push) Successful in 6s
All checks were successful
技能自动化发布 / release (push) Successful in 6s
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)。"""
|
||||
"""占位 run:RpaVideoSession 示范 + 模板提示;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__":
|
||||
|
||||
@@ -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"):
|
||||
|
||||
81
tests/test_docs_standards.py
Normal file
81
tests/test_docs_standards.py
Normal 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()
|
||||
@@ -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"])
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
161
tests/test_video_service.py
Normal 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()
|
||||
Reference in New Issue
Block a user