# -*- coding: utf-8 -*- """media_assets 共享媒体资源解析测试。""" from __future__ import annotations import json import platform import zipfile from pathlib import Path from unittest.mock import patch import pytest from jiangchang_skill_core import media_assets as ma def _make_complete_assets(root: Path, *, with_ffmpeg: bool = True, with_ffprobe: bool = True) -> None: (root / "music" / "calm").mkdir(parents=True) (root / "music" / "calm" / "track.mp3").write_bytes(b"x" * 256) (root / "fonts").mkdir(parents=True) (root / "watermark").mkdir(parents=True) bin_key = ma._platform_bin_key() bin_dir = root / "bin" / bin_key bin_dir.mkdir(parents=True) if with_ffmpeg: (bin_dir / ma._tool_filename("ffmpeg")).write_bytes(b"ffmpeg") if with_ffprobe: (bin_dir / ma._tool_filename("ffprobe")).write_bytes(b"ffprobe") def _sample_manifest() -> dict: bin_key = ma._platform_bin_key() return { "name": "media-assets", "version": "0.2.0", "directories": {"music": "music", "fonts": "fonts", "watermark": "watermark"}, "tools": { "ffmpeg": { bin_key: { "package": "ffmpeg-release-essentials", "archive_type": "zip", "urls": [{"name": "test", "url": "https://example.test/ffmpeg.zip"}], "archive_paths": { "ffmpeg": "*/bin/ffmpeg.exe", "ffprobe": "*/bin/ffprobe.exe", }, } } }, } def _install_tools_from_manifest(root: Path, warnings: list[str]) -> bool: bin_key = ma._platform_bin_key() bin_dir = root / "bin" / bin_key bin_dir.mkdir(parents=True, exist_ok=True) (bin_dir / ma._tool_filename("ffmpeg")).write_bytes(b"ffmpeg") (bin_dir / ma._tool_filename("ffprobe")).write_bytes(b"ffprobe") return True def _build_archive_zip(dest: Path, inner_name: str) -> None: manifest = _sample_manifest() with zipfile.ZipFile(dest, "w") as zf: zf.writestr(f"{inner_name}/README.md", "# media-assets\n") zf.writestr(f"{inner_name}/manifest.json", json.dumps(manifest, ensure_ascii=False)) zf.writestr(f"{inner_name}/music/calm/track.mp3", b"x" * 256) zf.writestr(f"{inner_name}/fonts/.keep", "") zf.writestr(f"{inner_name}/watermark/.keep", "") def test_default_bundle_url_is_release() -> None: url = ma._media_assets_bundle_url() assert url == ma.MEDIA_ASSETS_BUNDLE_URL assert url.endswith("/releases/download/vlatest/media-assets.zip") assert "/archive/main.zip" not in url def test_bundle_url_env_override() -> None: override = "https://example.com/media-assets.zip" assert ma._media_assets_bundle_url(env={"MEDIA_ASSETS_BUNDLE_URL": override}) == override def test_zip_download_uses_bundle_url_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: data_root = tmp_path / "data" shared = data_root / "shared" target = shared / "media-assets" archive = tmp_path / "media-assets.zip" override = "https://example.com/media-assets.zip" _build_archive_zip(archive, "media-assets") captured_urls: list[str] = [] def _fake_download(url: str, dest: Path) -> None: captured_urls.append(url) dest.write_bytes(archive.read_bytes()) monkeypatch.setattr(ma, "_download_zip", _fake_download) monkeypatch.setattr(ma, "_find_git_executable", lambda: None) monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest) env = { "JIANGCHANG_DATA_ROOT": str(data_root), "MEDIA_ASSETS_BUNDLE_URL": override, } status = ma.ensure_media_assets(env=env) assert captured_urls == [override] assert target.is_dir() assert status.ready is True def test_media_assets_root_env_priority(tmp_path: Path) -> None: custom = tmp_path / "custom-media" custom.mkdir() env = { "MEDIA_ASSETS_ROOT": str(custom), "JIANGCHANG_DATA_ROOT": str(tmp_path / "data"), } assert ma.resolve_media_assets_root(env=env) == custom def test_default_media_assets_path(tmp_path: Path) -> None: env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")} expected = tmp_path / "data" / "shared" / "media-assets" assert ma.resolve_media_assets_root(env=env) == expected def test_complete_assets_skips_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root) def _fail_download(*args, **kwargs): raise AssertionError("download should not be called") monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download) monkeypatch.setattr(ma, "_try_download_via_git", _fail_download) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is True assert status.warnings == [] assert status.source == "local" def test_missing_ffmpeg_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root, with_ffmpeg=False) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is False assert "ffmpeg_missing" in status.warnings def test_missing_ffprobe_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root, with_ffprobe=False) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is False assert "ffprobe_missing" in status.warnings def test_pick_background_music_stable_first(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root) music = root / "music" (music / "b-upbeat.mp3").write_bytes(b"x" * 256) (music / "calm" / "a-calm.mp3").write_bytes(b"x" * 256) (music / "z-last.wav").write_bytes(b"x" * 256) monkeypatch.setattr( ma, "ensure_media_assets", lambda *a, **k: ma._inspect_media_assets(root, source="local"), ) picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert picked == music / "b-upbeat.mp3" @pytest.mark.parametrize("inner_name", ["media-assets", "media-assets-main"]) def test_zip_wrapper_layout_detected( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, inner_name: str, ) -> None: data_root = tmp_path / "data" shared = data_root / "shared" target = shared / "media-assets" archive = tmp_path / "media-assets.zip" _build_archive_zip(archive, inner_name) def _fake_download(url: str, dest: Path) -> None: dest.write_bytes(archive.read_bytes()) monkeypatch.setattr(ma, "_download_zip", _fake_download) monkeypatch.setattr(ma, "_find_git_executable", lambda: None) monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(data_root)}) assert target.is_dir() assert status.ready is True assert status.source == "zip" assert "media_assets_zip_invalid_layout" not in status.warnings def test_zip_download_failure_returns_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import urllib.error def _raise_url_error(url: str, dest: Path) -> None: raise urllib.error.URLError("network down") monkeypatch.setattr(ma, "_download_zip", _raise_url_error) monkeypatch.setattr(ma, "_find_git_executable", lambda: None) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is False assert "media_assets_zip_download_failed" in status.warnings assert "git_not_available" in status.warnings def test_git_not_available_when_zip_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import urllib.error monkeypatch.setattr( ma, "_download_zip", lambda url, dest: (_ for _ in ()).throw(urllib.error.URLError("fail")), ) monkeypatch.setattr(ma, "_find_git_executable", lambda: None) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert "git_not_available" in status.warnings def test_media_assets_root_override_no_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: custom = tmp_path / "override" custom.mkdir() def _fail_download(*args, **kwargs): raise AssertionError("download should not be called when MEDIA_ASSETS_ROOT is set") monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download) status = ma.ensure_media_assets(env={"MEDIA_ASSETS_ROOT": str(custom)}) assert status.root == custom assert status.exists is True @pytest.mark.skipif(platform.system().lower() != "windows", reason="Windows default path check") def test_windows_default_data_root() -> None: with patch.dict("os.environ", {}, clear=True): root = ma.resolve_media_assets_root() assert root == Path(r"D:\jiangchang-data") / "shared" / "media-assets" def test_probe_media_assets_does_not_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def _fail_download(*args, **kwargs): raise AssertionError("download should not be called") monkeypatch.setattr(ma, "_try_download_via_zip", _fail_download) monkeypatch.setattr(ma, "_try_download_via_git", _fail_download) status = ma.probe_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.exists is False assert status.ffmpeg_path is None def test_lfs_pointer_warning_in_probe_and_ensure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root) music = root / "music" (music / "calm" / "track.mp3").unlink() (music / "lfs-only.mp3").write_text( "version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n" ) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) probed = ma.probe_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert "background_music_lfs_pointer_only" in probed.warnings assert probed.music_ready is False ensured = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert "background_music_lfs_pointer_only" in ensured.warnings assert ensured.ready is False def test_pick_background_music_skips_lfs_pointer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root, with_ffmpeg=True, with_ffprobe=True) music = root / "music" (music / "calm" / "track.mp3").unlink() (music / "real.mp3").write_bytes(b"x" * 256) lfs = music / "lfs-only.mp3" lfs.write_text("version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n") monkeypatch.setattr( ma, "ensure_media_assets", lambda *a, **k: ma._inspect_media_assets(root, source="local"), ) picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert picked == music / "real.mp3" def test_pick_background_music_no_valid_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" _make_complete_assets(root) music = root / "music" (music / "calm" / "track.mp3").unlink() (music / "pointer.mp3").write_text( "version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n" ) monkeypatch.setattr( ma, "ensure_media_assets", lambda *a, **k: ma._inspect_media_assets(root, source="local"), ) picked = ma.pick_background_music(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert picked is None def test_video_finalize_without_music_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import asyncio import os import tempfile from pathlib import Path from jiangchang_skill_core import config from jiangchang_skill_core.rpa.video_session import RpaVideoSession ffmpeg_exe = tmp_path / "ffmpeg.exe" ffmpeg_exe.write_bytes(b"ffmpeg") def _fake_resolve_ffmpeg(*args, **kwargs): return ffmpeg_exe def _fake_pick_music(*args, **kwargs): return None monkeypatch.setattr( "jiangchang_skill_core.rpa.video_session.resolve_ffmpeg", _fake_resolve_ffmpeg, ) monkeypatch.setattr( "jiangchang_skill_core.rpa.video_session.pick_background_music", _fake_pick_music, ) monkeypatch.setattr( "jiangchang_skill_core.rpa.video_session.background_music_issue", lambda *a, **k: None, ) os.environ["OPENCLAW_RECORD_VIDEO"] = "1" try: config.reset_cache() with tempfile.TemporaryDirectory() as skill_data: session = RpaVideoSession( skill_slug="receive-order", skill_data_dir=skill_data, batch_id="batch_no_music", ) capture = Path(session._capture_path) capture.parent.mkdir(parents=True, exist_ok=True) capture.write_bytes(b"\x00" * 64) monkeypatch.setattr( "jiangchang_skill_core.rpa.video_session._compose_capture_mp4", lambda *a, **k: (True, ""), ) asyncio.run(session.finalize()) assert "background_music_missing" in session.warnings assert session.output_video_path is not None finally: os.environ.pop("OPENCLAW_RECORD_VIDEO", None) config.reset_cache() def test_manifest_ffmpeg_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = tmp_path / "data" / "shared" / "media-assets" (root / "music" / "calm").mkdir(parents=True) (root / "music" / "calm" / "track.mp3").write_bytes(b"x" * 256) (root / "fonts").mkdir(parents=True) (root / "watermark").mkdir(parents=True) (root / "manifest.json").write_text( json.dumps(_sample_manifest(), ensure_ascii=False), encoding="utf-8", ) monkeypatch.setattr(ma, "_try_download_via_zip", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_via_git", lambda *a, **k: False) monkeypatch.setattr(ma, "_try_download_ffmpeg_tools", _install_tools_from_manifest) status = ma.ensure_media_assets(env={"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}) assert status.ready is True assert status.ffmpeg_path is not None assert status.ffprobe_path is not None