from __future__ import annotations import json import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from biliup_next.core.models import Artifact, Task, utc_now_iso from biliup_next.infra.adapters.codex_cli import CodexCliAdapter from biliup_next.modules.song_detect.providers.codex import CodexSongDetector from biliup_next.modules.song_detect.providers.qwen_cli import QwenCliSongDetector class FakeQwenCliAdapter: def __init__(self, returncode: int = 0) -> None: self.returncode = returncode self.last_qwen_cmd: str | None = None def run_song_detect(self, *, qwen_cmd: str, work_dir: Path, prompt: str): # noqa: ANN001 self.last_qwen_cmd = qwen_cmd songs_json_path = work_dir / "songs.json" songs_json_path.write_text( json.dumps( { "songs": [ { "start": "00:01:23,000", "end": "00:03:45,000", "title": "测试歌曲", "artist": "测试歌手", "confidence": 0.93, "evidence": "歌词命中", } ] }, ensure_ascii=False, ), encoding="utf-8", ) return type("Result", (), {"returncode": self.returncode, "stdout": "ok", "stderr": ""})() class FakeCodexCliAdapter: def __init__(self, returncode: int = 0) -> None: self.returncode = returncode def run_song_detect(self, *, codex_cmd: str, work_dir: Path, prompt: str): # noqa: ANN001 songs_json_path = work_dir / "songs.json" songs_json_path.write_text( json.dumps( { "songs": [ { "start": "00:01:23,000", "end": "00:03:45,000", "title": "测试歌曲", "artist": "测试歌手", "confidence": 0.93, "evidence": "歌词命中", } ] }, ensure_ascii=False, ), encoding="utf-8", ) return type("Result", (), {"returncode": self.returncode, "stdout": "codex stdout", "stderr": "codex stderr"})() class SongDetectProviderTests(unittest.TestCase): def test_qwen_cli_provider_generates_json_and_txt_artifacts(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: work_dir = Path(tmpdir) subtitle_path = work_dir / "subtitle.srt" subtitle_path.write_text("1\n00:00:00,000 --> 00:00:03,000\n测试字幕\n", encoding="utf-8") provider = QwenCliSongDetector(adapter=FakeQwenCliAdapter()) task = Task( id="task-1", source_type="local_file", source_path=str(work_dir / "video.mp4"), title="task-1", status="transcribed", created_at=utc_now_iso(), updated_at=utc_now_iso(), ) subtitle = Artifact( id=None, task_id=task.id, artifact_type="subtitle_srt", path=str(subtitle_path), metadata_json=None, created_at=utc_now_iso(), ) songs_json, songs_txt = provider.detect(task, subtitle, {"qwen_cmd": "qwen"}) self.assertEqual(json.loads(songs_json.metadata_json)["provider"], "qwen_cli") self.assertEqual(json.loads(songs_txt.metadata_json)["provider"], "qwen_cli") self.assertTrue(Path(songs_json.path).exists()) self.assertTrue(Path(songs_txt.path).exists()) self.assertIn("测试歌曲", Path(songs_txt.path).read_text(encoding="utf-8")) def test_codex_provider_writes_execution_output_to_session_log(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: work_dir = Path(tmpdir) subtitle_path = work_dir / "subtitle.srt" subtitle_path.write_text("1\n00:00:00,000 --> 00:00:03,000\n测试字幕\n", encoding="utf-8") provider = CodexSongDetector(adapter=FakeCodexCliAdapter()) task = Task( id="task-1", source_type="local_file", source_path=str(work_dir / "video.mp4"), title="task-1", status="transcribed", created_at=utc_now_iso(), updated_at=utc_now_iso(), ) subtitle = Artifact( id=None, task_id=task.id, artifact_type="subtitle_srt", path=str(subtitle_path), metadata_json=None, created_at=utc_now_iso(), ) songs_json, songs_txt = provider.detect(task, subtitle, {"codex_cmd": "codex"}) json_metadata = json.loads(songs_json.metadata_json) txt_metadata = json.loads(songs_txt.metadata_json) self.assertEqual(json_metadata["provider"], "codex") self.assertEqual(txt_metadata["provider"], "codex") self.assertNotIn("execution", json_metadata) codex_log = work_dir / "codex.log" self.assertTrue(codex_log.exists()) log_text = codex_log.read_text(encoding="utf-8") self.assertIn("returncode: 0", log_text) self.assertIn("codex stdout", log_text) self.assertIn("codex stderr", log_text) def test_codex_cli_adapter_disables_inner_sandbox_and_normalizes_proxy_env(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: calls = [] def fake_run(cmd, **kwargs): # noqa: ANN001 calls.append((cmd, kwargs)) return type("Result", (), {"returncode": 0, "stdout": "", "stderr": ""})() with patch.dict(os.environ, {"HTTPS_PROXY": "192.168.1.100:7897"}, clear=True): with patch("subprocess.run", side_effect=fake_run): CodexCliAdapter().run_song_detect( codex_cmd="codex", work_dir=Path(tmpdir), prompt="detect songs", ) cmd, kwargs = calls[0] self.assertIn("--dangerously-bypass-approvals-and-sandbox", cmd) self.assertNotIn("--full-auto", cmd) self.assertNotIn("workspace-write", cmd) self.assertEqual(kwargs["env"]["HTTPS_PROXY"], "http://192.168.1.100:7897") if __name__ == "__main__": unittest.main()