chore: ffmpeg record log, remove legacy video fields, logging fallback
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 19s
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 19s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,4 @@
|
|||||||
"""RPA 运行录屏会话:ffmpeg 桌面录制 → 字幕 + 背景音乐 → 最终 MP4。
|
"""RPA 运行录屏会话:ffmpeg 桌面录制 → 字幕 + 背景音乐 → 最终 MP4。"""
|
||||||
|
|
||||||
Playwright 仅用于浏览器自动化,不负责录屏。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -15,7 +12,7 @@ import time
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, IO, List, Optional
|
||||||
|
|
||||||
from .. import config
|
from .. import config
|
||||||
|
|
||||||
@@ -222,8 +219,6 @@ class RpaVideoSession:
|
|||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
self.output_video_path: Optional[str] = None
|
self.output_video_path: Optional[str] = None
|
||||||
self.capture_path: Optional[str] = None
|
self.capture_path: Optional[str] = None
|
||||||
# 已废弃:勿用于 Playwright;始终为 None。
|
|
||||||
self.record_video_dir: Optional[str] = None
|
|
||||||
self.artifacts: Dict[str, Any] = {}
|
self.artifacts: Dict[str, Any] = {}
|
||||||
|
|
||||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
@@ -236,6 +231,7 @@ class RpaVideoSession:
|
|||||||
)
|
)
|
||||||
self._subtitles_dir = os.path.join(self._artifact_root, "subtitles")
|
self._subtitles_dir = os.path.join(self._artifact_root, "subtitles")
|
||||||
self._logs_dir = os.path.join(self._artifact_root, "logs")
|
self._logs_dir = os.path.join(self._artifact_root, "logs")
|
||||||
|
self._ffmpeg_record_log_path = os.path.join(self._logs_dir, "ffmpeg-record.log")
|
||||||
self._capture_path = os.path.join(self._artifact_root, "capture.mp4")
|
self._capture_path = os.path.join(self._artifact_root, "capture.mp4")
|
||||||
self._srt_path = os.path.join(self._subtitles_dir, f"{self._video_stem}.srt")
|
self._srt_path = os.path.join(self._subtitles_dir, f"{self._video_stem}.srt")
|
||||||
self._planned_output = os.path.join(self._videos_dir, f"{self._video_stem}.mp4")
|
self._planned_output = os.path.join(self._videos_dir, f"{self._video_stem}.mp4")
|
||||||
@@ -243,6 +239,7 @@ class RpaVideoSession:
|
|||||||
self._steps: List[_StepEntry] = []
|
self._steps: List[_StepEntry] = []
|
||||||
self._t0: Optional[float] = None
|
self._t0: Optional[float] = None
|
||||||
self._ffmpeg_proc: Optional[subprocess.Popen] = None
|
self._ffmpeg_proc: Optional[subprocess.Popen] = None
|
||||||
|
self._ffmpeg_record_log_file: Optional[IO[str]] = None
|
||||||
|
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
os.makedirs(self._videos_dir, exist_ok=True)
|
os.makedirs(self._videos_dir, exist_ok=True)
|
||||||
@@ -250,6 +247,21 @@ class RpaVideoSession:
|
|||||||
os.makedirs(self._logs_dir, exist_ok=True)
|
os.makedirs(self._logs_dir, exist_ok=True)
|
||||||
os.makedirs(self._artifact_root, exist_ok=True)
|
os.makedirs(self._artifact_root, exist_ok=True)
|
||||||
|
|
||||||
|
def _record_log_hint(self) -> str:
|
||||||
|
if self.enabled and self._ffmpeg_record_log_path:
|
||||||
|
return f"; record_log={self._ffmpeg_record_log_path}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _append_warning(self, code: str, detail: str = "") -> None:
|
||||||
|
msg = code if not detail else f"{code}: {detail}"
|
||||||
|
if code in (
|
||||||
|
"ffmpeg_record_start_failed",
|
||||||
|
"ffmpeg_capture_missing",
|
||||||
|
"ffmpeg_compose_failed",
|
||||||
|
):
|
||||||
|
msg += self._record_log_hint()
|
||||||
|
self.warnings.append(msg)
|
||||||
|
|
||||||
def step(self, text: str, *, duration: float = 4.0) -> None:
|
def step(self, text: str, *, duration: float = 4.0) -> None:
|
||||||
self.add_step(text, duration=duration)
|
self.add_step(text, duration=duration)
|
||||||
|
|
||||||
@@ -262,6 +274,16 @@ class RpaVideoSession:
|
|||||||
elapsed = time.monotonic() - self._t0
|
elapsed = time.monotonic() - self._t0
|
||||||
self._steps.append(_StepEntry(start=elapsed, text=text.strip(), duration=duration))
|
self._steps.append(_StepEntry(start=elapsed, text=text.strip(), duration=duration))
|
||||||
|
|
||||||
|
def _close_ffmpeg_record_log(self) -> None:
|
||||||
|
fh = self._ffmpeg_record_log_file
|
||||||
|
self._ffmpeg_record_log_file = None
|
||||||
|
if fh is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
fh.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _start_ffmpeg_capture(self) -> None:
|
def _start_ffmpeg_capture(self) -> None:
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
@@ -271,26 +293,39 @@ class RpaVideoSession:
|
|||||||
|
|
||||||
cmd = _build_desktop_record_cmd(self._capture_path)
|
cmd = _build_desktop_record_cmd(self._capture_path)
|
||||||
if cmd is None:
|
if cmd is None:
|
||||||
self.warnings.append(
|
self._append_warning(
|
||||||
"ffmpeg_record_start_failed: desktop capture only supported on Windows"
|
"ffmpeg_record_start_failed",
|
||||||
|
"desktop capture only supported on Windows",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
stderr_target: Any = subprocess.DEVNULL
|
||||||
|
try:
|
||||||
|
os.makedirs(self._logs_dir, exist_ok=True)
|
||||||
|
self._ffmpeg_record_log_file = open(
|
||||||
|
self._ffmpeg_record_log_path, "a", encoding="utf-8"
|
||||||
|
)
|
||||||
|
stderr_target = self._ffmpeg_record_log_file
|
||||||
|
except Exception as exc:
|
||||||
|
self.warnings.append(f"ffmpeg_record_log_open_failed: {exc}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._ffmpeg_proc = subprocess.Popen(
|
self._ffmpeg_proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=stderr_target,
|
||||||
)
|
)
|
||||||
self.capture_path = self._capture_path
|
self.capture_path = self._capture_path
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.warnings.append(f"ffmpeg_record_start_failed: {exc}")
|
self._append_warning("ffmpeg_record_start_failed", str(exc))
|
||||||
self._ffmpeg_proc = None
|
self._ffmpeg_proc = None
|
||||||
|
self._close_ffmpeg_record_log()
|
||||||
|
|
||||||
def _stop_ffmpeg_capture(self) -> None:
|
def _stop_ffmpeg_capture(self) -> None:
|
||||||
proc = self._ffmpeg_proc
|
proc = self._ffmpeg_proc
|
||||||
if proc is None:
|
if proc is None:
|
||||||
|
self._close_ffmpeg_record_log()
|
||||||
return
|
return
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
try:
|
try:
|
||||||
@@ -314,8 +349,8 @@ class RpaVideoSession:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._ffmpeg_proc = None
|
self._ffmpeg_proc = None
|
||||||
await_sleep = 0.3
|
self._close_ffmpeg_record_log()
|
||||||
time.sleep(await_sleep)
|
time.sleep(0.3)
|
||||||
|
|
||||||
async def __aenter__(self) -> "RpaVideoSession":
|
async def __aenter__(self) -> "RpaVideoSession":
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
@@ -342,7 +377,7 @@ class RpaVideoSession:
|
|||||||
try:
|
try:
|
||||||
capture = Path(self._capture_path)
|
capture = Path(self._capture_path)
|
||||||
if not capture.is_file() or capture.stat().st_size <= 0:
|
if not capture.is_file() or capture.stat().st_size <= 0:
|
||||||
self.warnings.append("ffmpeg_capture_missing")
|
self._append_warning("ffmpeg_capture_missing")
|
||||||
self.artifacts = self.summary()
|
self.artifacts = self.summary()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -396,7 +431,7 @@ class RpaVideoSession:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
self.warnings.append(f"ffmpeg_compose_failed: {err}")
|
self._append_warning("ffmpeg_compose_failed", err)
|
||||||
self.artifacts = self.summary()
|
self.artifacts = self.summary()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -407,10 +442,16 @@ class RpaVideoSession:
|
|||||||
self.artifacts = self.summary()
|
self.artifacts = self.summary()
|
||||||
|
|
||||||
def summary(self) -> Dict[str, Any]:
|
def summary(self) -> Dict[str, Any]:
|
||||||
|
record_log = (
|
||||||
|
os.path.abspath(self._ffmpeg_record_log_path)
|
||||||
|
if self.enabled and self._ffmpeg_record_log_path
|
||||||
|
else None
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
"path": self.output_video_path,
|
"path": self.output_video_path,
|
||||||
"capture_path": self.capture_path,
|
"capture_path": self.capture_path,
|
||||||
|
"record_log_path": record_log,
|
||||||
"warnings": list(self.warnings),
|
"warnings": list(self.warnings),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ _logger_name: str = ""
|
|||||||
|
|
||||||
def get_unified_logs_dir() -> str:
|
def get_unified_logs_dir() -> str:
|
||||||
path = os.path.join(get_data_root(), get_user_id(), "logs")
|
path = os.path.join(get_data_root(), get_user_id(), "logs")
|
||||||
os.makedirs(path, exist_ok=True)
|
try:
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +33,10 @@ def get_skill_log_file_path() -> str:
|
|||||||
if override:
|
if override:
|
||||||
parent = os.path.dirname(os.path.abspath(override))
|
parent = os.path.dirname(os.path.abspath(override))
|
||||||
if parent:
|
if parent:
|
||||||
os.makedirs(parent, exist_ok=True)
|
try:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
return os.path.abspath(override)
|
return os.path.abspath(override)
|
||||||
return os.path.join(get_unified_logs_dir(), "jiangchang.log")
|
return os.path.join(get_unified_logs_dir(), "jiangchang.log")
|
||||||
|
|
||||||
@@ -81,10 +87,16 @@ def _backup_count() -> int:
|
|||||||
return 30
|
return 30
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_fallback_handler(log: logging.Logger, fmt: logging.Formatter) -> None:
|
||||||
|
"""文件日志不可用时仅挂内存 handler,不向 stderr 打印 logging 异常。"""
|
||||||
|
log.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
|
||||||
def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
|
def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
幂等:为指定 logger 挂载统一文件日志与可选 stderr。
|
幂等:为指定 logger 挂载统一文件日志与可选 stderr。
|
||||||
须在进程早期调用(如 CLI main 在业务日志之前)。
|
须在进程早期调用(如 CLI main 在业务日志之前)。
|
||||||
|
文件日志目录无权限或不可写时降级为 no-op,不影响 CLI 返回码。
|
||||||
"""
|
"""
|
||||||
global _skill_slug, _logger_name
|
global _skill_slug, _logger_name
|
||||||
ensure_trace_for_process()
|
ensure_trace_for_process()
|
||||||
@@ -95,30 +107,41 @@ def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
|
|||||||
if log.handlers:
|
if log.handlers:
|
||||||
return
|
return
|
||||||
log.setLevel(_log_level_from_env())
|
log.setLevel(_log_level_from_env())
|
||||||
path = get_skill_log_file_path()
|
|
||||||
fh = TimedRotatingFileHandler(
|
|
||||||
path,
|
|
||||||
when="midnight",
|
|
||||||
interval=1,
|
|
||||||
backupCount=_backup_count(),
|
|
||||||
encoding="utf-8",
|
|
||||||
delay=True,
|
|
||||||
)
|
|
||||||
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||||
fh.setFormatter(fmt)
|
filt = _SkillContextFilter()
|
||||||
fh.addFilter(_SkillContextFilter())
|
|
||||||
log.addHandler(fh)
|
try:
|
||||||
|
path = get_skill_log_file_path()
|
||||||
|
fh = TimedRotatingFileHandler(
|
||||||
|
path,
|
||||||
|
when="midnight",
|
||||||
|
interval=1,
|
||||||
|
backupCount=_backup_count(),
|
||||||
|
encoding="utf-8",
|
||||||
|
delay=True,
|
||||||
|
)
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
fh.addFilter(filt)
|
||||||
|
log.addHandler(fh)
|
||||||
|
except (OSError, PermissionError, ValueError):
|
||||||
|
_attach_fallback_handler(log, fmt)
|
||||||
|
except Exception:
|
||||||
|
_attach_fallback_handler(log, fmt)
|
||||||
|
|
||||||
if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in (
|
if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in (
|
||||||
"1",
|
"1",
|
||||||
"true",
|
"true",
|
||||||
"yes",
|
"yes",
|
||||||
"on",
|
"on",
|
||||||
):
|
):
|
||||||
sh = logging.StreamHandler(sys.stderr)
|
try:
|
||||||
sh.setLevel(logging.WARNING)
|
sh = logging.StreamHandler(sys.stderr)
|
||||||
sh.setFormatter(fmt)
|
sh.setLevel(logging.WARNING)
|
||||||
sh.addFilter(_SkillContextFilter())
|
sh.setFormatter(fmt)
|
||||||
log.addHandler(sh)
|
sh.addFilter(filt)
|
||||||
|
log.addHandler(sh)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
log.propagate = False
|
log.propagate = False
|
||||||
|
|
||||||
|
|
||||||
@@ -144,13 +167,18 @@ def attach_unified_file_handler(
|
|||||||
lg = logging.getLogger(logger_name)
|
lg = logging.getLogger(logger_name)
|
||||||
lg.handlers.clear()
|
lg.handlers.clear()
|
||||||
lg.setLevel(level)
|
lg.setLevel(level)
|
||||||
parent = os.path.dirname(os.path.abspath(log_path))
|
|
||||||
if parent:
|
|
||||||
os.makedirs(parent, exist_ok=True)
|
|
||||||
fh = logging.FileHandler(log_path, encoding="utf-8")
|
|
||||||
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||||
fh.setFormatter(fmt)
|
try:
|
||||||
fh.addFilter(_SkillContextFilter())
|
parent = os.path.dirname(os.path.abspath(log_path))
|
||||||
lg.addHandler(fh)
|
if parent:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
fh = logging.FileHandler(log_path, encoding="utf-8")
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
fh.addFilter(_SkillContextFilter())
|
||||||
|
lg.addHandler(fh)
|
||||||
|
except (OSError, PermissionError, ValueError):
|
||||||
|
lg.addHandler(logging.NullHandler())
|
||||||
|
except Exception:
|
||||||
|
lg.addHandler(logging.NullHandler())
|
||||||
lg.propagate = False
|
lg.propagate = False
|
||||||
return lg
|
return lg
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""RpaVideoSession:ffmpeg 桌面录屏,无 Playwright record_video_dir。"""
|
"""RpaVideoSession:ffmpeg 桌面录屏。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -28,18 +28,17 @@ class TestRpaVideoSession(unittest.TestCase):
|
|||||||
batch_id="batch_test",
|
batch_id="batch_test",
|
||||||
)
|
)
|
||||||
self.assertFalse(session.enabled)
|
self.assertFalse(session.enabled)
|
||||||
self.assertIsNone(session.record_video_dir)
|
|
||||||
self.assertIsNone(session.output_video_path)
|
|
||||||
self.assertIsNone(session.capture_path)
|
|
||||||
summ = session.summary()
|
summ = session.summary()
|
||||||
self.assertFalse(summ["enabled"])
|
self.assertFalse(summ["enabled"])
|
||||||
self.assertIsNone(summ["path"])
|
self.assertIsNone(summ["path"])
|
||||||
|
self.assertIsNone(summ["capture_path"])
|
||||||
|
self.assertIsNone(summ["record_log_path"])
|
||||||
finally:
|
finally:
|
||||||
if old is not None:
|
if old is not None:
|
||||||
os.environ["OPENCLAW_RECORD_VIDEO"] = old
|
os.environ["OPENCLAW_RECORD_VIDEO"] = old
|
||||||
config.reset_cache()
|
config.reset_cache()
|
||||||
|
|
||||||
def test_enabled_paths_no_raw_video(self) -> None:
|
def test_enabled_paths_use_ffmpeg_capture(self) -> None:
|
||||||
os.environ["OPENCLAW_RECORD_VIDEO"] = "1"
|
os.environ["OPENCLAW_RECORD_VIDEO"] = "1"
|
||||||
try:
|
try:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
@@ -51,12 +50,16 @@ class TestRpaVideoSession(unittest.TestCase):
|
|||||||
title="接收订单",
|
title="接收订单",
|
||||||
)
|
)
|
||||||
self.assertTrue(session.enabled)
|
self.assertTrue(session.enabled)
|
||||||
self.assertIsNone(session.record_video_dir)
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
session._capture_path.replace("\\", "/").endswith(
|
session._capture_path.replace("\\", "/").endswith(
|
||||||
"/rpa-artifacts/exmail_20260602_120000/capture.mp4"
|
"/rpa-artifacts/exmail_20260602_120000/capture.mp4"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
session._ffmpeg_record_log_path.replace("\\", "/").endswith(
|
||||||
|
"/rpa-artifacts/exmail_20260602_120000/logs/ffmpeg-record.log"
|
||||||
|
)
|
||||||
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
session._planned_output.replace("\\", "/").startswith(
|
session._planned_output.replace("\\", "/").startswith(
|
||||||
tmp.replace("\\", "/") + "/videos/"
|
tmp.replace("\\", "/") + "/videos/"
|
||||||
@@ -87,15 +90,17 @@ class TestRpaVideoSession(unittest.TestCase):
|
|||||||
batch_id="batch_ff",
|
batch_id="batch_ff",
|
||||||
) as video:
|
) as video:
|
||||||
video.add_step("测试步骤")
|
video.add_step("测试步骤")
|
||||||
with patch.object(video, "finalize", return_value=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
asyncio.run(_run())
|
with patch.object(RpaVideoSession, "finalize", return_value=None):
|
||||||
|
asyncio.run(_run())
|
||||||
|
|
||||||
self.assertTrue(mock_popen.called)
|
self.assertTrue(mock_popen.called)
|
||||||
cmd = mock_popen.call_args[0][0]
|
cmd = mock_popen.call_args[0][0]
|
||||||
self.assertIn("ffmpeg", cmd)
|
self.assertIn("ffmpeg", cmd)
|
||||||
self.assertIn("gdigrab", cmd)
|
self.assertIn("gdigrab", cmd)
|
||||||
self.assertIn("desktop", cmd)
|
self.assertIn("desktop", cmd)
|
||||||
|
kwargs = mock_popen.call_args[1]
|
||||||
|
self.assertIsNot(kwargs.get("stderr"), os.devnull)
|
||||||
finally:
|
finally:
|
||||||
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
||||||
config.reset_cache()
|
config.reset_cache()
|
||||||
@@ -106,16 +111,6 @@ class TestRpaVideoSession(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
config.reset_cache()
|
config.reset_cache()
|
||||||
|
|
||||||
async def _run() -> None:
|
|
||||||
async with RpaVideoSession(
|
|
||||||
skill_slug="receive-order",
|
|
||||||
skill_data_dir=tmp,
|
|
||||||
batch_id="batch_no_ff",
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
asyncio.run(_run())
|
|
||||||
session = RpaVideoSession(
|
session = RpaVideoSession(
|
||||||
skill_slug="receive-order",
|
skill_slug="receive-order",
|
||||||
skill_data_dir=tmp,
|
skill_data_dir=tmp,
|
||||||
@@ -138,10 +133,11 @@ class TestRpaVideoSession(unittest.TestCase):
|
|||||||
batch_id="batch_warn",
|
batch_id="batch_warn",
|
||||||
)
|
)
|
||||||
asyncio.run(session.finalize())
|
asyncio.run(session.finalize())
|
||||||
self.assertIn("ffmpeg_capture_missing", session.warnings)
|
self.assertTrue(
|
||||||
|
any("ffmpeg_capture_missing" in w for w in session.warnings)
|
||||||
|
)
|
||||||
|
self.assertIn("record_log=", "".join(session.warnings))
|
||||||
self.assertIsNone(session.output_video_path)
|
self.assertIsNone(session.output_video_path)
|
||||||
summ = session.summary()
|
|
||||||
self.assertIsNone(summ["path"])
|
|
||||||
finally:
|
finally:
|
||||||
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
os.environ.pop("OPENCLAW_RECORD_VIDEO", None)
|
||||||
config.reset_cache()
|
config.reset_cache()
|
||||||
|
|||||||
Reference in New Issue
Block a user