"""Tests for Bilibili video pipeline integration Tests for: - Bilibili video processing flow (uses SRTProcessor) - Bilibili article processing flow (uses AnalyzeProcessor) - Zhihu processing flow (uses AnalyzeProcessor) - Platform routing logic """ import asyncio import json from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from kb_workflow.pipeline.pipeline import Pipeline from kb_workflow.pipeline.schemas import PipelineConfig from kb_workflow.pipeline.processors import ( ParseProcessor, DownloadProcessor, ASRProcessor, AnalyzeProcessor, ArchiveProcessor ) from kb_workflow.pipeline.processors.srt import SRTProcessor # ============================================================================= # Test Fixtures # ============================================================================= @pytest.fixture def mock_bilibili_video_url(): """B站视频 URL""" return "https://www.bilibili.com/video/BV1test123456" @pytest.fixture def mock_bilibili_article_url(): """B站文章 URL""" return "https://www.bilibili.com/read/cv123456" @pytest.fixture def mock_zhihu_answer_url(): """知乎回答 URL""" return "https://www.zhihu.com/question/123456/answer/789012" @pytest.fixture def sample_metadata(): """示例元数据""" return { "title": "测试视频标题", "author": "测试作者", "duration": 300, "pubtime": "2024-01-01", "bvid": "BV1test123456", "platform": "bilibili", "url": "https://www.bilibili.com/video/BV1test123456" } @pytest.fixture def sample_srt_content(): """示例 SRT 内容""" return """1 00:00:00,000 --> 00:00:05,000 第一句字幕内容 2 00:00:05,000 --> 00:00:10,000 第二句字幕内容 3 00:00:10,000 --> 00:00:15,000 第三句字幕内容 """ # ============================================================================= # Test Cases # ============================================================================= class TestBilibiliVideoPipeline: """B站视频处理流程测试""" @pytest.mark.asyncio async def test_bilibili_video_uses_srt_processor(self, tmp_path, mock_bilibili_video_url, sample_metadata, sample_srt_content): """验证 B站视频使用 SRTProcessor 而非 AnalyzeProcessor""" # Setup: 创建临时目录和文件 content_dir = tmp_path / "bilibili_BV1test123456" content_dir.mkdir(parents=True) # 创建元数据文件 metadata_file = content_dir / "metadata.json" metadata_file.write_text(json.dumps(sample_metadata, ensure_ascii=False), encoding='utf-8') # 创建 SRT 文件 srt_file = content_dir / "bilibili_BV1test123456_subtitle.srt" srt_file.write_text(sample_srt_content, encoding='utf-8') # Mock processors with patch.object(ParseProcessor, 'process', new_callable=AsyncMock) as mock_parse, \ patch.object(DownloadProcessor, 'process', new_callable=AsyncMock) as mock_download, \ patch.object(ASRProcessor, 'process', new_callable=AsyncMock) as mock_asr, \ patch.object(SRTProcessor, 'process', new_callable=AsyncMock) as mock_srt, \ patch.object(AnalyzeProcessor, 'process', new_callable=AsyncMock) as mock_analyze, \ patch.object(ArchiveProcessor, 'process', new_callable=AsyncMock) as mock_archive: # 配置 mock 返回值 from kb_workflow.pipeline.schemas import ParseOutput, DownloadOutput, ASROutput, SRTOutput, ArchiveOutput mock_parse.return_value = ParseOutput( success=True, platform="bilibili", content_type="video", content_id="BV1test123456", resolved_url=mock_bilibili_video_url, original_url=mock_bilibili_video_url ) mock_download.return_value = DownloadOutput( success=True, metadata_file=str(metadata_file), content_file=str(content_dir / "video.mp4"), title=sample_metadata["title"], author=sample_metadata["author"], duration=sample_metadata["duration"] ) mock_asr.return_value = ASROutput( success=True, tool="subtitle", has_subtitle=True, output_files={"srt": str(srt_file), "json": str(content_dir / "subtitle.json")}, text="第一句字幕内容 第二句字幕内容 第三句字幕内容", duration_seconds=15.0 ) mock_srt.return_value = SRTOutput( success=True, output_file=str(content_dir / "analysis.md"), paragraphs=["第一句字幕内容", "第二句字幕内容", "第三句字幕内容"], tags=[], corrected=False ) mock_archive.return_value = ArchiveOutput( success=True, archive_file=str(tmp_path / "archive" / "test.md"), skipped=False, platform="bilibili" ) # 创建 Pipeline processors = [ ParseProcessor(), DownloadProcessor(), ASRProcessor(), SRTProcessor(), AnalyzeProcessor(), ArchiveProcessor(), ] config = PipelineConfig( steps=["parse", "download", "asr", "srt", "analyze", "archive"], output_dir=tmp_path, skip_completed=False, ) pipeline = Pipeline( processors=processors, config=config, output_dir=tmp_path, ) # 执行 result = await pipeline.run(mock_bilibili_video_url) # 验证 assert result.success, f"Pipeline failed: {result.error_message}" # SRTProcessor 应该被调用 mock_srt.assert_called_once() # AnalyzeProcessor 不应该被调用(B站视频跳过) mock_analyze.assert_not_called() @pytest.mark.asyncio async def test_bilibili_article_uses_analyze_processor(self, tmp_path, mock_bilibili_article_url, sample_metadata): """验证 B站文章使用 AnalyzeProcessor""" # Setup content_dir = tmp_path / "bilibili_cv123456" content_dir.mkdir(parents=True) metadata_file = content_dir / "metadata.json" sample_metadata["url"] = mock_bilibili_article_url metadata_file.write_text(json.dumps(sample_metadata, ensure_ascii=False), encoding='utf-8') content_file = content_dir / "article.txt" content_file.write_text("文章内容测试", encoding='utf-8') with patch.object(ParseProcessor, 'process', new_callable=AsyncMock) as mock_parse, \ patch.object(DownloadProcessor, 'process', new_callable=AsyncMock) as mock_download, \ patch.object(ASRProcessor, 'process', new_callable=AsyncMock) as mock_asr, \ patch.object(SRTProcessor, 'process', new_callable=AsyncMock) as mock_srt, \ patch.object(AnalyzeProcessor, 'process', new_callable=AsyncMock) as mock_analyze, \ patch.object(ArchiveProcessor, 'process', new_callable=AsyncMock) as mock_archive: from kb_workflow.pipeline.schemas import ParseOutput, DownloadOutput, AnalyzeOutput, ArchiveOutput mock_parse.return_value = ParseOutput( success=True, platform="bilibili", content_type="article", content_id="cv123456", resolved_url=mock_bilibili_article_url, original_url=mock_bilibili_article_url ) mock_download.return_value = DownloadOutput( success=True, metadata_file=str(metadata_file), content_file=str(content_file), title=sample_metadata["title"], author=sample_metadata["author"] ) mock_analyze.return_value = AnalyzeOutput( success=True, analysis_file=str(content_dir / "analysis.md"), summary="文章摘要", main_topics=["主题1"], tags=["标签1"] ) mock_archive.return_value = ArchiveOutput( success=True, archive_file=str(tmp_path / "archive" / "test.md"), skipped=False, platform="bilibili" ) processors = [ ParseProcessor(), DownloadProcessor(), ASRProcessor(), SRTProcessor(), AnalyzeProcessor(), ArchiveProcessor(), ] config = PipelineConfig( steps=["parse", "download", "asr", "srt", "analyze", "archive"], output_dir=tmp_path, skip_completed=False, ) pipeline = Pipeline( processors=processors, config=config, output_dir=tmp_path, ) result = await pipeline.run(mock_bilibili_article_url) assert result.success, f"Pipeline failed: {result.error_message}" # ASR 不应该被调用(文章类型跳过) mock_asr.assert_not_called() # SRTProcessor 不应该被调用(非视频跳过) mock_srt.assert_not_called() # AnalyzeProcessor 应该被调用 mock_analyze.assert_called_once() class TestZhihuPipeline: """知乎处理流程测试""" @pytest.mark.asyncio async def test_zhihu_answer_uses_analyze_processor(self, tmp_path, mock_zhihu_answer_url): """验证知乎回答使用 AnalyzeProcessor""" content_dir = tmp_path / "zhihu_789012" content_dir.mkdir(parents=True) metadata = { "title": "知乎回答测试", "author": "知乎用户", "platform": "zhihu", "url": mock_zhihu_answer_url } metadata_file = content_dir / "metadata.json" metadata_file.write_text(json.dumps(metadata, ensure_ascii=False), encoding='utf-8') content_file = content_dir / "content.txt" content_file.write_text("知乎回答内容测试", encoding='utf-8') with patch.object(ParseProcessor, 'process', new_callable=AsyncMock) as mock_parse, \ patch.object(DownloadProcessor, 'process', new_callable=AsyncMock) as mock_download, \ patch.object(ASRProcessor, 'process', new_callable=AsyncMock) as mock_asr, \ patch.object(SRTProcessor, 'process', new_callable=AsyncMock) as mock_srt, \ patch.object(AnalyzeProcessor, 'process', new_callable=AsyncMock) as mock_analyze, \ patch.object(ArchiveProcessor, 'process', new_callable=AsyncMock) as mock_archive: from kb_workflow.pipeline.schemas import ParseOutput, DownloadOutput, AnalyzeOutput, ArchiveOutput mock_parse.return_value = ParseOutput( success=True, platform="zhihu", content_type="answer", content_id="789012", resolved_url=mock_zhihu_answer_url, original_url=mock_zhihu_answer_url ) mock_download.return_value = DownloadOutput( success=True, metadata_file=str(metadata_file), content_file=str(content_file), title=metadata["title"], author=metadata["author"] ) mock_analyze.return_value = AnalyzeOutput( success=True, analysis_file=str(content_dir / "analysis.md"), summary="回答摘要", main_topics=["主题1"], tags=["标签1"] ) mock_archive.return_value = ArchiveOutput( success=True, archive_file=str(tmp_path / "archive" / "test.md"), skipped=False, platform="zhihu" ) processors = [ ParseProcessor(), DownloadProcessor(), ASRProcessor(), SRTProcessor(), AnalyzeProcessor(), ArchiveProcessor(), ] config = PipelineConfig( steps=["parse", "download", "asr", "srt", "analyze", "archive"], output_dir=tmp_path, skip_completed=False, ) pipeline = Pipeline( processors=processors, config=config, output_dir=tmp_path, ) result = await pipeline.run(mock_zhihu_answer_url) assert result.success, f"Pipeline failed: {result.error_message}" # ASR 不应该被调用(文本类型跳过) mock_asr.assert_not_called() # SRTProcessor 不应该被调用(非B站视频跳过) mock_srt.assert_not_called() # AnalyzeProcessor 应该被调用 mock_analyze.assert_called_once() class TestMarkdownOutput: """Markdown 输出格式测试""" def test_srt_output_markdown_format(self, tmp_path, sample_metadata, sample_srt_content): """验证生成的 Markdown 文件格式正确""" # 创建 SRT 文件 srt_file = tmp_path / "test.srt" srt_file.write_text(sample_srt_content, encoding='utf-8') # 创建元数据文件 metadata_file = tmp_path / "metadata.json" metadata_file.write_text(json.dumps(sample_metadata, ensure_ascii=False), encoding='utf-8') # 创建输出文件路径 output_file = tmp_path / "analysis.md" # 使用 SRTProcessor 处理 from kb_workflow.pipeline.processors.srt import SRTProcessor from kb_workflow.pipeline.schemas import SRTInput import asyncio processor = SRTProcessor(enable_correction=False) step_input = SRTInput( content_id="BV1test123456", platform="bilibili", srt_file=str(srt_file), metadata_file=str(metadata_file), output_file=str(output_file), content_type="video" ) result = asyncio.run(processor.process(step_input)) assert result.success assert output_file.exists() # 验证文件内容 content = output_file.read_text(encoding='utf-8') # 检查 frontmatter assert "---" in content assert "title:" in content assert "date:" in content assert "author:" in content assert "platform: bilibili" in content # 检查内容结构 assert "## 摘要" in content or "## 内容" in content