refactor(06-02): update CLI and test imports to kb_workflow.pipeline
- Remove sys.path manipulation from kb_workflow.py - Update CLI imports to use kb_workflow.pipeline prefix - Update all test file imports and patch paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,13 +43,13 @@ def process_command(
|
|||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from kb_pipeline.config import get_settings
|
from kb_workflow.pipeline.config import get_settings
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.processors import (
|
from kb_workflow.pipeline.processors import (
|
||||||
ASRProcessor, AnalyzeProcessor, ArchiveProcessor,
|
ASRProcessor, AnalyzeProcessor, ArchiveProcessor,
|
||||||
DownloadProcessor, ParseProcessor
|
DownloadProcessor, ParseProcessor
|
||||||
)
|
)
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
async def run_pipeline():
|
async def run_pipeline():
|
||||||
# Determine output directory - default to intermediate_path
|
# Determine output directory - default to intermediate_path
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ from pathlib import Path
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.schemas import PipelineConfig, PipelineOutput, WorkflowState
|
from kb_workflow.pipeline.schemas import PipelineConfig, PipelineOutput, WorkflowState
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -93,14 +93,14 @@ class TestFullWorkflowExecution:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_full_workflow_executes_successfully(self, temp_content_dir, mock_processors):
|
async def test_full_workflow_executes_successfully(self, temp_content_dir, mock_processors):
|
||||||
"""Test that full workflow executes from URL to analysis."""
|
"""Test that full workflow executes from URL to analysis."""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
steps=["parse", "download", "asr", "analyze"],
|
steps=["parse", "download", "asr", "analyze"],
|
||||||
output_dir=temp_content_dir,
|
output_dir=temp_content_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
@@ -118,14 +118,14 @@ class TestFullWorkflowExecution:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_workflow_creates_content_directory(self, temp_content_dir, mock_processors):
|
async def test_workflow_creates_content_directory(self, temp_content_dir, mock_processors):
|
||||||
"""Test that workflow creates content directory with correct naming."""
|
"""Test that workflow creates content directory with correct naming."""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
steps=["parse"],
|
steps=["parse"],
|
||||||
output_dir=temp_content_dir,
|
output_dir=temp_content_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
@@ -147,7 +147,7 @@ class TestWorkflowErrorHandling:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_workflow_handles_errors_gracefully(self, temp_content_dir):
|
async def test_workflow_handles_errors_gracefully(self, temp_content_dir):
|
||||||
"""Test that workflow handles errors gracefully."""
|
"""Test that workflow handles errors gracefully."""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
# Create processor that fails
|
# Create processor that fails
|
||||||
failing_processor = MagicMock()
|
failing_processor = MagicMock()
|
||||||
@@ -162,7 +162,7 @@ class TestWorkflowErrorHandling:
|
|||||||
output_dir=temp_content_dir,
|
output_dir=temp_content_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
@@ -179,14 +179,14 @@ class TestWorkflowErrorHandling:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_workflow_handles_invalid_url(self, temp_content_dir):
|
async def test_workflow_handles_invalid_url(self, temp_content_dir):
|
||||||
"""Test that workflow handles invalid URL."""
|
"""Test that workflow handles invalid URL."""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
steps=["parse"],
|
steps=["parse"],
|
||||||
output_dir=temp_content_dir,
|
output_dir=temp_content_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
# Empty processor list - pipeline will fail to parse URL
|
# Empty processor list - pipeline will fail to parse URL
|
||||||
@@ -271,7 +271,7 @@ class TestWorkflowResumability:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_workflow_skips_completed_steps(self, temp_content_dir):
|
async def test_workflow_skips_completed_steps(self, temp_content_dir):
|
||||||
"""Test that workflow can resume (skip completed steps)."""
|
"""Test that workflow can resume (skip completed steps)."""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
# Create state with parse already completed
|
# Create state with parse already completed
|
||||||
state_manager = WorkflowStateManager(temp_content_dir)
|
state_manager = WorkflowStateManager(temp_content_dir)
|
||||||
@@ -303,7 +303,7 @@ class TestWorkflowResumability:
|
|||||||
skip_completed=True,
|
skip_completed=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock(intermediate_path=str(temp_content_dir))
|
mock_settings.return_value = MagicMock(intermediate_path=str(temp_content_dir))
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
@@ -324,7 +324,7 @@ class TestWorkflowResumability:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_workflow_reruns_all_steps_when_skip_disabled(self, temp_content_dir):
|
async def test_workflow_reruns_all_steps_when_skip_disabled(self, temp_content_dir):
|
||||||
"""Test that workflow re-runs all steps when skip_completed is False."""
|
"""Test that workflow re-runs all steps when skip_completed is False."""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
# Create processors
|
# Create processors
|
||||||
parse_processor = MagicMock()
|
parse_processor = MagicMock()
|
||||||
@@ -337,7 +337,7 @@ class TestWorkflowResumability:
|
|||||||
skip_completed=False, # Disable skip
|
skip_completed=False, # Disable skip
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from pydantic import ValidationError as PydanticValidationError
|
from pydantic import ValidationError as PydanticValidationError
|
||||||
|
|
||||||
from kb_pipeline.config import KBSettings, get_settings
|
from kb_workflow.pipeline.config import KBSettings, get_settings
|
||||||
|
|
||||||
|
|
||||||
class TestKBSettings:
|
class TestKBSettings:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Tests for KBError, NetworkError, ValidationError, SystemError, AuthError and Err
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from kb_pipeline.errors import (
|
from kb_workflow.pipeline.errors import (
|
||||||
KBError,
|
KBError,
|
||||||
NetworkError,
|
NetworkError,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class TestIdempotencyChecker:
|
|||||||
|
|
||||||
def test_check_exists_returns_true_when_files_exist(self, tmp_path):
|
def test_check_exists_returns_true_when_files_exist(self, tmp_path):
|
||||||
"""Test 1: 文件存在时 check_exists 返回 (True, [file_list])"""
|
"""Test 1: 文件存在时 check_exists 返回 (True, [file_list])"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
# Create output directory with expected files
|
# Create output directory with expected files
|
||||||
output_dir = tmp_path / "bilibili"
|
output_dir = tmp_path / "bilibili"
|
||||||
@@ -41,7 +41,7 @@ class TestIdempotencyChecker:
|
|||||||
|
|
||||||
def test_check_exists_returns_false_when_files_not_exist(self, tmp_path):
|
def test_check_exists_returns_false_when_files_not_exist(self, tmp_path):
|
||||||
"""Test 2: 文件不存在时 check_exists 返回 (False, [])"""
|
"""Test 2: 文件不存在时 check_exists 返回 (False, [])"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "bilibili"
|
output_dir = tmp_path / "bilibili"
|
||||||
output_dir.mkdir()
|
output_dir.mkdir()
|
||||||
@@ -57,7 +57,7 @@ class TestIdempotencyChecker:
|
|||||||
|
|
||||||
def test_check_exists_returns_partial_files_when_some_exist(self, tmp_path):
|
def test_check_exists_returns_partial_files_when_some_exist(self, tmp_path):
|
||||||
"""Test 3: 部分文件存在时返回正确的存在文件列表"""
|
"""Test 3: 部分文件存在时返回正确的存在文件列表"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "zhihu"
|
output_dir = tmp_path / "zhihu"
|
||||||
output_dir.mkdir()
|
output_dir.mkdir()
|
||||||
@@ -80,7 +80,7 @@ class TestIdempotencyChecker:
|
|||||||
|
|
||||||
def test_check_exists_creates_directory_if_not_exists(self, tmp_path):
|
def test_check_exists_creates_directory_if_not_exists(self, tmp_path):
|
||||||
"""如果输出目录不存在,check_exists 应该返回 False"""
|
"""如果输出目录不存在,check_exists 应该返回 False"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "new_platform" # Directory doesn't exist
|
output_dir = tmp_path / "new_platform" # Directory doesn't exist
|
||||||
content_id = "BV1test123"
|
content_id = "BV1test123"
|
||||||
@@ -98,7 +98,7 @@ class TestIdempotencyCheckerFileNaming:
|
|||||||
|
|
||||||
def test_bilibili_file_naming(self, tmp_path):
|
def test_bilibili_file_naming(self, tmp_path):
|
||||||
"""测试B站文件的命名格式"""
|
"""测试B站文件的命名格式"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "bilibili"
|
output_dir = tmp_path / "bilibili"
|
||||||
output_dir.mkdir()
|
output_dir.mkdir()
|
||||||
@@ -121,7 +121,7 @@ class TestIdempotencyCheckerFileNaming:
|
|||||||
|
|
||||||
def test_zhihu_file_naming(self, tmp_path):
|
def test_zhihu_file_naming(self, tmp_path):
|
||||||
"""测试知乎文件的命名格式"""
|
"""测试知乎文件的命名格式"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "zhihu"
|
output_dir = tmp_path / "zhihu"
|
||||||
output_dir.mkdir()
|
output_dir.mkdir()
|
||||||
@@ -148,7 +148,7 @@ class TestIdempotencyCheckerEdgeCases:
|
|||||||
|
|
||||||
def test_check_exists_with_empty_content_id(self, tmp_path):
|
def test_check_exists_with_empty_content_id(self, tmp_path):
|
||||||
"""空content_id应该返回False"""
|
"""空content_id应该返回False"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "bilibili"
|
output_dir = tmp_path / "bilibili"
|
||||||
output_dir.mkdir()
|
output_dir.mkdir()
|
||||||
@@ -161,7 +161,7 @@ class TestIdempotencyCheckerEdgeCases:
|
|||||||
|
|
||||||
def test_check_exists_with_special_characters_in_id(self, tmp_path):
|
def test_check_exists_with_special_characters_in_id(self, tmp_path):
|
||||||
"""ID中包含特殊字符时的处理"""
|
"""ID中包含特殊字符时的处理"""
|
||||||
from kb_pipeline.idempotency import IdempotencyChecker
|
from kb_workflow.pipeline.idempotency import IdempotencyChecker
|
||||||
|
|
||||||
output_dir = tmp_path / "bilibili"
|
output_dir = tmp_path / "bilibili"
|
||||||
output_dir.mkdir()
|
output_dir.mkdir()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TestLinkParserBilibiliVideo:
|
|||||||
|
|
||||||
def test_parse_bilibili_video_url_returns_correct_parsed_link(self):
|
def test_parse_bilibili_video_url_returns_correct_parsed_link(self):
|
||||||
"""Test 1: 解析B站视频URL返回正确的 ParsedLink (platform=bilibili, type=video, id=BVxxx)"""
|
"""Test 1: 解析B站视频URL返回正确的 ParsedLink (platform=bilibili, type=video, id=BVxxx)"""
|
||||||
from kb_pipeline.link_parser import LinkParser, ParsedLink
|
from kb_workflow.pipeline.link_parser import LinkParser, ParsedLink
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://www.bilibili.com/video/BV1GJ411x7Qw")
|
result = parser.parse("https://www.bilibili.com/video/BV1GJ411x7Qw")
|
||||||
@@ -31,7 +31,7 @@ class TestLinkParserBilibiliVideo:
|
|||||||
|
|
||||||
def test_parse_bilibili_video_url_without_www(self):
|
def test_parse_bilibili_video_url_without_www(self):
|
||||||
"""B站视频URL不带www也能正确解析"""
|
"""B站视频URL不带www也能正确解析"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://bilibili.com/video/BV1GJ411x7Qw")
|
result = parser.parse("https://bilibili.com/video/BV1GJ411x7Qw")
|
||||||
@@ -43,7 +43,7 @@ class TestLinkParserBilibiliVideo:
|
|||||||
|
|
||||||
def test_parse_bilibili_video_url_with_http(self):
|
def test_parse_bilibili_video_url_with_http(self):
|
||||||
"""B站视频URL使用http也能正确解析"""
|
"""B站视频URL使用http也能正确解析"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("http://www.bilibili.com/video/BV1GJ411x7Qw")
|
result = parser.parse("http://www.bilibili.com/video/BV1GJ411x7Qw")
|
||||||
@@ -58,7 +58,7 @@ class TestLinkParserBilibiliArticle:
|
|||||||
|
|
||||||
def test_parse_bilibili_article_url_returns_correct_parsed_link(self):
|
def test_parse_bilibili_article_url_returns_correct_parsed_link(self):
|
||||||
"""Test 2: 解析B站专栏URL返回正确的 ParsedLink (platform=bilibili, type=article, id=cvxxx)"""
|
"""Test 2: 解析B站专栏URL返回正确的 ParsedLink (platform=bilibili, type=article, id=cvxxx)"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://www.bilibili.com/read/cv12345678")
|
result = parser.parse("https://www.bilibili.com/read/cv12345678")
|
||||||
@@ -70,7 +70,7 @@ class TestLinkParserBilibiliArticle:
|
|||||||
|
|
||||||
def test_parse_bilibili_article_url_with_article_prefix(self):
|
def test_parse_bilibili_article_url_with_article_prefix(self):
|
||||||
"""B站专栏URL使用article前缀也能正确解析"""
|
"""B站专栏URL使用article前缀也能正确解析"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://www.bilibili.com/article/cv12345678")
|
result = parser.parse("https://www.bilibili.com/article/cv12345678")
|
||||||
@@ -86,7 +86,7 @@ class TestLinkParserZhihuAnswer:
|
|||||||
|
|
||||||
def test_parse_zhihu_answer_url_returns_correct_parsed_link(self):
|
def test_parse_zhihu_answer_url_returns_correct_parsed_link(self):
|
||||||
"""Test 3: 解析知乎问答URL返回正确的 ParsedLink (platform=zhihu, type=answer, id=xxx)"""
|
"""Test 3: 解析知乎问答URL返回正确的 ParsedLink (platform=zhihu, type=answer, id=xxx)"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://www.zhihu.com/question/12345678/answer/87654321")
|
result = parser.parse("https://www.zhihu.com/question/12345678/answer/87654321")
|
||||||
@@ -98,7 +98,7 @@ class TestLinkParserZhihuAnswer:
|
|||||||
|
|
||||||
def test_parse_zhihu_answer_url_without_www(self):
|
def test_parse_zhihu_answer_url_without_www(self):
|
||||||
"""知乎问答URL不带www也能正确解析"""
|
"""知乎问答URL不带www也能正确解析"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://zhihu.com/question/12345678/answer/87654321")
|
result = parser.parse("https://zhihu.com/question/12345678/answer/87654321")
|
||||||
@@ -113,7 +113,7 @@ class TestLinkParserZhihuZhuanlan:
|
|||||||
|
|
||||||
def test_parse_zhihu_zhuanlan_url_returns_correct_parsed_link(self):
|
def test_parse_zhihu_zhuanlan_url_returns_correct_parsed_link(self):
|
||||||
"""Test 4: 解析知乎专栏URL返回正确的 ParsedLink (platform=zhihu, type=zhuanlan, id=xxx)"""
|
"""Test 4: 解析知乎专栏URL返回正确的 ParsedLink (platform=zhihu, type=zhuanlan, id=xxx)"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://zhuanlan.zhihu.com/p/12345678")
|
result = parser.parse("https://zhuanlan.zhihu.com/p/12345678")
|
||||||
@@ -127,10 +127,10 @@ class TestLinkParserZhihuZhuanlan:
|
|||||||
class TestLinkParserShortLink:
|
class TestLinkParserShortLink:
|
||||||
"""Tests for short link resolution"""
|
"""Tests for short link resolution"""
|
||||||
|
|
||||||
@patch("kb_pipeline.link_parser.requests.head")
|
@patch("kb_workflow.pipeline.link_parser.requests.head")
|
||||||
def test_parse_b23tv_shortlink_resolves_to_real_url(self, mock_head):
|
def test_parse_b23tv_shortlink_resolves_to_real_url(self, mock_head):
|
||||||
"""Test 5: 解析b23.tv短链返回解析后的真实URL"""
|
"""Test 5: 解析b23.tv短链返回解析后的真实URL"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
# Mock the redirect response
|
# Mock the redirect response
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
@@ -147,10 +147,10 @@ class TestLinkParserShortLink:
|
|||||||
assert result.content_id == "BV1GJ411x7Qw"
|
assert result.content_id == "BV1GJ411x7Qw"
|
||||||
assert result.resolved_url == "https://www.bilibili.com/video/BV1GJ411x7Qw"
|
assert result.resolved_url == "https://www.bilibili.com/video/BV1GJ411x7Qw"
|
||||||
|
|
||||||
@patch("kb_pipeline.link_parser.requests.head")
|
@patch("kb_workflow.pipeline.link_parser.requests.head")
|
||||||
def test_shortlink_resolution_uses_allow_redirects(self, mock_head):
|
def test_shortlink_resolution_uses_allow_redirects(self, mock_head):
|
||||||
"""短链解析应该使用 allow_redirects=True"""
|
"""短链解析应该使用 allow_redirects=True"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.status_code = 200
|
mock_response.status_code = 200
|
||||||
@@ -170,7 +170,7 @@ class TestLinkParserUnknownURL:
|
|||||||
|
|
||||||
def test_parse_unknown_url_returns_none(self):
|
def test_parse_unknown_url_returns_none(self):
|
||||||
"""Test 6: 无法识别的URL返回None(不抛异常)"""
|
"""Test 6: 无法识别的URL返回None(不抛异常)"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
result = parser.parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
||||||
@@ -179,7 +179,7 @@ class TestLinkParserUnknownURL:
|
|||||||
|
|
||||||
def test_parse_invalid_url_returns_none(self):
|
def test_parse_invalid_url_returns_none(self):
|
||||||
"""无效URL返回None"""
|
"""无效URL返回None"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("not a valid url")
|
result = parser.parse("not a valid url")
|
||||||
@@ -188,7 +188,7 @@ class TestLinkParserUnknownURL:
|
|||||||
|
|
||||||
def test_parse_empty_string_returns_none(self):
|
def test_parse_empty_string_returns_none(self):
|
||||||
"""空字符串返回None"""
|
"""空字符串返回None"""
|
||||||
from kb_pipeline.link_parser import LinkParser
|
from kb_workflow.pipeline.link_parser import LinkParser
|
||||||
|
|
||||||
parser = LinkParser()
|
parser = LinkParser()
|
||||||
result = parser.parse("")
|
result = parser.parse("")
|
||||||
@@ -201,7 +201,7 @@ class TestParsedLink:
|
|||||||
|
|
||||||
def test_parsed_link_has_all_required_fields(self):
|
def test_parsed_link_has_all_required_fields(self):
|
||||||
"""ParsedLink 应包含所有必需字段"""
|
"""ParsedLink 应包含所有必需字段"""
|
||||||
from kb_pipeline.link_parser import ParsedLink
|
from kb_workflow.pipeline.link_parser import ParsedLink
|
||||||
|
|
||||||
link = ParsedLink(
|
link = ParsedLink(
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
@@ -219,7 +219,7 @@ class TestParsedLink:
|
|||||||
|
|
||||||
def test_parsed_link_resolved_url_defaults_to_original(self):
|
def test_parsed_link_resolved_url_defaults_to_original(self):
|
||||||
"""ParsedLink 的 resolved_url 默认等于 original_url"""
|
"""ParsedLink 的 resolved_url 默认等于 original_url"""
|
||||||
from kb_pipeline.link_parser import ParsedLink
|
from kb_workflow.pipeline.link_parser import ParsedLink
|
||||||
|
|
||||||
link = ParsedLink(
|
link = ParsedLink(
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class TestPipelineInput:
|
|||||||
|
|
||||||
def test_schema_pipeline_input_validates_url_field(self):
|
def test_schema_pipeline_input_validates_url_field(self):
|
||||||
"""PipelineInput must include url field (required)"""
|
"""PipelineInput must include url field (required)"""
|
||||||
from kb_pipeline.schemas import PipelineInput
|
from kb_workflow.pipeline.schemas import PipelineInput
|
||||||
|
|
||||||
# Missing url
|
# Missing url
|
||||||
with pytest.raises(ValidationError) as exc_info:
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
@@ -37,7 +37,7 @@ class TestPipelineInput:
|
|||||||
|
|
||||||
def test_schema_pipeline_input_has_optional_output_dir(self):
|
def test_schema_pipeline_input_has_optional_output_dir(self):
|
||||||
"""PipelineInput has optional output_dir field"""
|
"""PipelineInput has optional output_dir field"""
|
||||||
from kb_pipeline.schemas import PipelineInput
|
from kb_workflow.pipeline.schemas import PipelineInput
|
||||||
|
|
||||||
# Without output_dir
|
# Without output_dir
|
||||||
input_data = PipelineInput(url="https://www.bilibili.com/video/BV1xx")
|
input_data = PipelineInput(url="https://www.bilibili.com/video/BV1xx")
|
||||||
@@ -52,7 +52,7 @@ class TestPipelineInput:
|
|||||||
|
|
||||||
def test_schema_pipeline_input_has_skip_completed_default(self):
|
def test_schema_pipeline_input_has_skip_completed_default(self):
|
||||||
"""PipelineInput skip_completed defaults to True"""
|
"""PipelineInput skip_completed defaults to True"""
|
||||||
from kb_pipeline.schemas import PipelineInput
|
from kb_workflow.pipeline.schemas import PipelineInput
|
||||||
|
|
||||||
input_data = PipelineInput(url="https://www.bilibili.com/video/BV1xx")
|
input_data = PipelineInput(url="https://www.bilibili.com/video/BV1xx")
|
||||||
assert input_data.skip_completed is True
|
assert input_data.skip_completed is True
|
||||||
@@ -70,7 +70,7 @@ class TestPipelineOutput:
|
|||||||
|
|
||||||
def test_schema_pipeline_output_contains_results_dict(self):
|
def test_schema_pipeline_output_contains_results_dict(self):
|
||||||
"""PipelineOutput contains results dict"""
|
"""PipelineOutput contains results dict"""
|
||||||
from kb_pipeline.schemas import PipelineOutput
|
from kb_workflow.pipeline.schemas import PipelineOutput
|
||||||
|
|
||||||
output = PipelineOutput(success=True)
|
output = PipelineOutput(success=True)
|
||||||
assert hasattr(output, "results")
|
assert hasattr(output, "results")
|
||||||
@@ -79,7 +79,7 @@ class TestPipelineOutput:
|
|||||||
|
|
||||||
def test_schema_pipeline_output_has_success_field(self):
|
def test_schema_pipeline_output_has_success_field(self):
|
||||||
"""PipelineOutput requires success field"""
|
"""PipelineOutput requires success field"""
|
||||||
from kb_pipeline.schemas import PipelineOutput
|
from kb_workflow.pipeline.schemas import PipelineOutput
|
||||||
|
|
||||||
with pytest.raises(ValidationError) as exc_info:
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
PipelineOutput()
|
PipelineOutput()
|
||||||
@@ -87,7 +87,7 @@ class TestPipelineOutput:
|
|||||||
|
|
||||||
def test_schema_pipeline_output_has_optional_error_message(self):
|
def test_schema_pipeline_output_has_optional_error_message(self):
|
||||||
"""PipelineOutput has optional error_message field"""
|
"""PipelineOutput has optional error_message field"""
|
||||||
from kb_pipeline.schemas import PipelineOutput
|
from kb_workflow.pipeline.schemas import PipelineOutput
|
||||||
|
|
||||||
output = PipelineOutput(success=True)
|
output = PipelineOutput(success=True)
|
||||||
assert output.error_message is None
|
assert output.error_message is None
|
||||||
@@ -100,7 +100,7 @@ class TestPipelineOutput:
|
|||||||
|
|
||||||
def test_schema_pipeline_output_has_content_dir(self):
|
def test_schema_pipeline_output_has_content_dir(self):
|
||||||
"""PipelineOutput has optional content_dir field"""
|
"""PipelineOutput has optional content_dir field"""
|
||||||
from kb_pipeline.schemas import PipelineOutput
|
from kb_workflow.pipeline.schemas import PipelineOutput
|
||||||
|
|
||||||
output = PipelineOutput(success=True)
|
output = PipelineOutput(success=True)
|
||||||
assert output.content_dir is None
|
assert output.content_dir is None
|
||||||
@@ -117,7 +117,7 @@ class TestPipelineConfig:
|
|||||||
|
|
||||||
def test_schema_pipeline_config_has_default_steps_list(self):
|
def test_schema_pipeline_config_has_default_steps_list(self):
|
||||||
"""PipelineConfig has default steps list"""
|
"""PipelineConfig has default steps list"""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig()
|
config = PipelineConfig()
|
||||||
assert hasattr(config, "steps")
|
assert hasattr(config, "steps")
|
||||||
@@ -126,14 +126,14 @@ class TestPipelineConfig:
|
|||||||
|
|
||||||
def test_schema_pipeline_config_allows_custom_steps(self):
|
def test_schema_pipeline_config_allows_custom_steps(self):
|
||||||
"""PipelineConfig allows custom steps list"""
|
"""PipelineConfig allows custom steps list"""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
custom_config = PipelineConfig(steps=["parse", "download"])
|
custom_config = PipelineConfig(steps=["parse", "download"])
|
||||||
assert custom_config.steps == ["parse", "download"]
|
assert custom_config.steps == ["parse", "download"]
|
||||||
|
|
||||||
def test_schema_pipeline_config_allows_custom_output_dir(self):
|
def test_schema_pipeline_config_allows_custom_output_dir(self):
|
||||||
"""PipelineConfig allows custom output_dir"""
|
"""PipelineConfig allows custom output_dir"""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig(output_dir=Path("/custom/output"))
|
config = PipelineConfig(output_dir=Path("/custom/output"))
|
||||||
assert config.output_dir == Path("/custom/output")
|
assert config.output_dir == Path("/custom/output")
|
||||||
@@ -144,14 +144,14 @@ class TestPipelineConfig:
|
|||||||
|
|
||||||
def test_schema_pipeline_config_has_skip_completed_default(self):
|
def test_schema_pipeline_config_has_skip_completed_default(self):
|
||||||
"""PipelineConfig skip_completed defaults to True"""
|
"""PipelineConfig skip_completed defaults to True"""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig()
|
config = PipelineConfig()
|
||||||
assert config.skip_completed is True
|
assert config.skip_completed is True
|
||||||
|
|
||||||
def test_schema_pipeline_config_has_max_retries_default(self):
|
def test_schema_pipeline_config_has_max_retries_default(self):
|
||||||
"""PipelineConfig max_retries defaults to 3"""
|
"""PipelineConfig max_retries defaults to 3"""
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig()
|
config = PipelineConfig()
|
||||||
assert config.max_retries == 3
|
assert config.max_retries == 3
|
||||||
@@ -171,8 +171,8 @@ class TestPipelineInit:
|
|||||||
|
|
||||||
def test_init_pipeline_with_processors(self):
|
def test_init_pipeline_with_processors(self):
|
||||||
"""Pipeline initializes with all processors"""
|
"""Pipeline initializes with all processors"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
mock_processor1 = MagicMock()
|
mock_processor1 = MagicMock()
|
||||||
mock_processor1.STEP_NAME = "parse"
|
mock_processor1.STEP_NAME = "parse"
|
||||||
@@ -180,7 +180,7 @@ class TestPipelineInit:
|
|||||||
mock_processor2 = MagicMock()
|
mock_processor2 = MagicMock()
|
||||||
mock_processor2.STEP_NAME = "download"
|
mock_processor2.STEP_NAME = "download"
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(processors=[mock_processor1, mock_processor2])
|
pipeline = Pipeline(processors=[mock_processor1, mock_processor2])
|
||||||
@@ -192,15 +192,15 @@ class TestPipelineInit:
|
|||||||
|
|
||||||
def test_init_pipeline_validates_processor_step_name(self):
|
def test_init_pipeline_validates_processor_step_name(self):
|
||||||
"""Pipeline validates processor STEP_NAME matches config.steps"""
|
"""Pipeline validates processor STEP_NAME matches config.steps"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
mock_processor = MagicMock()
|
mock_processor = MagicMock()
|
||||||
mock_processor.STEP_NAME = "unknown_step"
|
mock_processor.STEP_NAME = "unknown_step"
|
||||||
|
|
||||||
config = PipelineConfig(steps=["parse", "download"])
|
config = PipelineConfig(steps=["parse", "download"])
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
with pytest.raises(ValueError) as exc_info:
|
||||||
@@ -210,13 +210,13 @@ class TestPipelineInit:
|
|||||||
|
|
||||||
def test_init_pipeline_handles_missing_processor_gracefully(self):
|
def test_init_pipeline_handles_missing_processor_gracefully(self):
|
||||||
"""Pipeline handles missing processor gracefully"""
|
"""Pipeline handles missing processor gracefully"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
# Empty processors list should work
|
# Empty processors list should work
|
||||||
config = PipelineConfig(steps=[])
|
config = PipelineConfig(steps=[])
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(processors=[], config=config)
|
pipeline = Pipeline(processors=[], config=config)
|
||||||
@@ -228,14 +228,14 @@ class TestPipelineParseUrl:
|
|||||||
|
|
||||||
def test_parse_url_creates_correct_content_directory(self):
|
def test_parse_url_creates_correct_content_directory(self):
|
||||||
"""Pipeline._get_content_dir creates correct content directory"""
|
"""Pipeline._get_content_dir creates correct content directory"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.link_parser import ParsedLink
|
from kb_workflow.pipeline.link_parser import ParsedLink
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
mock_processor = MagicMock()
|
mock_processor = MagicMock()
|
||||||
mock_processor.STEP_NAME = "parse"
|
mock_processor.STEP_NAME = "parse"
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
@@ -258,12 +258,12 @@ class TestPipelineParseUrl:
|
|||||||
|
|
||||||
def test_parse_url_returns_none_for_empty_url(self):
|
def test_parse_url_returns_none_for_empty_url(self):
|
||||||
"""Pipeline._parse_url returns None for empty URL"""
|
"""Pipeline._parse_url returns None for empty URL"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
|
|
||||||
mock_processor = MagicMock()
|
mock_processor = MagicMock()
|
||||||
mock_processor.STEP_NAME = "parse"
|
mock_processor.STEP_NAME = "parse"
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(processors=[mock_processor])
|
pipeline = Pipeline(processors=[mock_processor])
|
||||||
@@ -273,12 +273,12 @@ class TestPipelineParseUrl:
|
|||||||
|
|
||||||
def test_parse_url_returns_none_for_invalid_url(self):
|
def test_parse_url_returns_none_for_invalid_url(self):
|
||||||
"""Pipeline._parse_url returns None for unrecognized URL"""
|
"""Pipeline._parse_url returns None for unrecognized URL"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
|
|
||||||
mock_processor = MagicMock()
|
mock_processor = MagicMock()
|
||||||
mock_processor.STEP_NAME = "parse"
|
mock_processor.STEP_NAME = "parse"
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(processors=[mock_processor])
|
pipeline = Pipeline(processors=[mock_processor])
|
||||||
@@ -288,12 +288,12 @@ class TestPipelineParseUrl:
|
|||||||
|
|
||||||
def test_parse_url_parses_bilibili_url(self):
|
def test_parse_url_parses_bilibili_url(self):
|
||||||
"""Pipeline._parse_url correctly parses Bilibili URL"""
|
"""Pipeline._parse_url correctly parses Bilibili URL"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
|
|
||||||
mock_processor = MagicMock()
|
mock_processor = MagicMock()
|
||||||
mock_processor.STEP_NAME = "parse"
|
mock_processor.STEP_NAME = "parse"
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
pipeline = Pipeline(processors=[mock_processor])
|
pipeline = Pipeline(processors=[mock_processor])
|
||||||
@@ -373,8 +373,8 @@ class TestPipelineArchiveStep:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pipeline_includes_archive_step(self, mock_processors_with_archive, tmp_path):
|
async def test_pipeline_includes_archive_step(self, mock_processors_with_archive, tmp_path):
|
||||||
"""Test that pipeline includes archive as final step"""
|
"""Test that pipeline includes archive as final step"""
|
||||||
from kb_pipeline.pipeline import Pipeline
|
from kb_workflow.pipeline.pipeline import Pipeline
|
||||||
from kb_pipeline.schemas import PipelineConfig
|
from kb_workflow.pipeline.schemas import PipelineConfig
|
||||||
|
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
steps=["parse", "download", "asr", "analyze", "archive"],
|
steps=["parse", "download", "asr", "analyze", "archive"],
|
||||||
@@ -382,7 +382,7 @@ class TestPipelineArchiveStep:
|
|||||||
skip_completed=False
|
skip_completed=False
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.pipeline.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.pipeline.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock(intermediate_path=str(tmp_path))
|
mock_settings.return_value = MagicMock(intermediate_path=str(tmp_path))
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
@@ -395,7 +395,7 @@ class TestPipelineArchiveStep:
|
|||||||
with patch.object(pipeline, '_parse_url') as mock_parse, \
|
with patch.object(pipeline, '_parse_url') as mock_parse, \
|
||||||
patch.object(pipeline, '_get_content_dir') as mock_dir:
|
patch.object(pipeline, '_get_content_dir') as mock_dir:
|
||||||
|
|
||||||
from kb_pipeline.link_parser import ParsedLink
|
from kb_workflow.pipeline.link_parser import ParsedLink
|
||||||
mock_parse.return_value = ParsedLink(
|
mock_parse.return_value = ParsedLink(
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
content_type="video",
|
content_type="video",
|
||||||
@@ -419,8 +419,8 @@ class TestArchivePlatformDirectory:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_archive_uses_platform_directory(self, tmp_path):
|
async def test_archive_uses_platform_directory(self, tmp_path):
|
||||||
"""Test that archive files go to {archive_path}/{platform}/ directory"""
|
"""Test that archive files go to {archive_path}/{platform}/ directory"""
|
||||||
from kb_pipeline.processors.archive import ArchiveProcessor
|
from kb_workflow.pipeline.processors.archive import ArchiveProcessor
|
||||||
from kb_pipeline.schemas import ArchiveInput
|
from kb_workflow.pipeline.schemas import ArchiveInput
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
archive_dir = tmp_path / "archive"
|
archive_dir = tmp_path / "archive"
|
||||||
@@ -430,7 +430,7 @@ class TestArchivePlatformDirectory:
|
|||||||
analysis_file.write_text("# Test")
|
analysis_file.write_text("# Test")
|
||||||
|
|
||||||
# Mock settings - patch at base module level where get_settings is called in __init__
|
# Mock settings - patch at base module level where get_settings is called in __init__
|
||||||
with patch('kb_pipeline.processors.base.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.base.get_settings') as mock_get_settings:
|
||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
mock_settings.archive_path = str(archive_dir)
|
mock_settings.archive_path = str(archive_dir)
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
@@ -461,8 +461,8 @@ class TestArchiveNoDateSubdirectories:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_archive_no_date_subdirectories(self, tmp_path):
|
async def test_archive_no_date_subdirectories(self, tmp_path):
|
||||||
"""Test that archive does not create date-based subdirectories"""
|
"""Test that archive does not create date-based subdirectories"""
|
||||||
from kb_pipeline.processors.archive import ArchiveProcessor
|
from kb_workflow.pipeline.processors.archive import ArchiveProcessor
|
||||||
from kb_pipeline.schemas import ArchiveInput
|
from kb_workflow.pipeline.schemas import ArchiveInput
|
||||||
|
|
||||||
archive_dir = tmp_path / "archive"
|
archive_dir = tmp_path / "archive"
|
||||||
content_dir = tmp_path / "intermediate" / "bilibili_BV1test"
|
content_dir = tmp_path / "intermediate" / "bilibili_BV1test"
|
||||||
@@ -470,7 +470,7 @@ class TestArchiveNoDateSubdirectories:
|
|||||||
analysis_file = content_dir / "analysis.md"
|
analysis_file = content_dir / "analysis.md"
|
||||||
analysis_file.write_text("# Test")
|
analysis_file.write_text("# Test")
|
||||||
|
|
||||||
with patch('kb_pipeline.processors.base.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.base.get_settings') as mock_get_settings:
|
||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
mock_settings.archive_path = str(archive_dir)
|
mock_settings.archive_path = str(archive_dir)
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import pytest
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from kb_pipeline.errors import NetworkError, ValidationError, ErrorType
|
from kb_workflow.pipeline.errors import NetworkError, ValidationError, ErrorType
|
||||||
from kb_pipeline.retry import retry_on_network_error
|
from kb_workflow.pipeline.retry import retry_on_network_error
|
||||||
|
|
||||||
|
|
||||||
class TestRetryOnNetworkError:
|
class TestRetryOnNetworkError:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class TestStepInput:
|
|||||||
|
|
||||||
def test_step_input_requires_content_id(self):
|
def test_step_input_requires_content_id(self):
|
||||||
"""StepInput 必须包含 content_id 字段"""
|
"""StepInput 必须包含 content_id 字段"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
|
|
||||||
with pytest.raises(ValidationError) as exc_info:
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
StepInput(platform="bilibili")
|
StepInput(platform="bilibili")
|
||||||
@@ -22,7 +22,7 @@ class TestStepInput:
|
|||||||
|
|
||||||
def test_step_input_requires_platform(self):
|
def test_step_input_requires_platform(self):
|
||||||
"""StepInput 必须包含 platform 字段"""
|
"""StepInput 必须包含 platform 字段"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
|
|
||||||
with pytest.raises(ValidationError) as exc_info:
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
StepInput(content_id="BV1test123")
|
StepInput(content_id="BV1test123")
|
||||||
@@ -31,7 +31,7 @@ class TestStepInput:
|
|||||||
|
|
||||||
def test_step_input_validates_platform_literal(self):
|
def test_step_input_validates_platform_literal(self):
|
||||||
"""StepInput platform 必须是 bilibili 或 zhihu"""
|
"""StepInput platform 必须是 bilibili 或 zhihu"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
|
|
||||||
# 有效值
|
# 有效值
|
||||||
step_bilibili = StepInput(content_id="BV1test", platform="bilibili")
|
step_bilibili = StepInput(content_id="BV1test", platform="bilibili")
|
||||||
@@ -46,7 +46,7 @@ class TestStepInput:
|
|||||||
|
|
||||||
def test_step_input_content_id_non_empty(self):
|
def test_step_input_content_id_non_empty(self):
|
||||||
"""StepInput content_id 不能为空字符串"""
|
"""StepInput content_id 不能为空字符串"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
StepInput(content_id="", platform="bilibili")
|
StepInput(content_id="", platform="bilibili")
|
||||||
@@ -57,7 +57,7 @@ class TestStepOutput:
|
|||||||
|
|
||||||
def test_step_output_requires_success(self):
|
def test_step_output_requires_success(self):
|
||||||
"""StepOutput 必须包含 success 字段"""
|
"""StepOutput 必须包含 success 字段"""
|
||||||
from kb_pipeline.schemas import StepOutput
|
from kb_workflow.pipeline.schemas import StepOutput
|
||||||
|
|
||||||
with pytest.raises(ValidationError) as exc_info:
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
StepOutput()
|
StepOutput()
|
||||||
@@ -66,7 +66,7 @@ class TestStepOutput:
|
|||||||
|
|
||||||
def test_step_output_default_completed_at(self):
|
def test_step_output_default_completed_at(self):
|
||||||
"""StepOutput completed_at 默认为当前时间"""
|
"""StepOutput completed_at 默认为当前时间"""
|
||||||
from kb_pipeline.schemas import StepOutput
|
from kb_workflow.pipeline.schemas import StepOutput
|
||||||
|
|
||||||
before = datetime.now()
|
before = datetime.now()
|
||||||
output = StepOutput(success=True)
|
output = StepOutput(success=True)
|
||||||
@@ -76,7 +76,7 @@ class TestStepOutput:
|
|||||||
|
|
||||||
def test_step_output_optional_error_message(self):
|
def test_step_output_optional_error_message(self):
|
||||||
"""StepOutput error_message 是可选的"""
|
"""StepOutput error_message 是可选的"""
|
||||||
from kb_pipeline.schemas import StepOutput
|
from kb_workflow.pipeline.schemas import StepOutput
|
||||||
|
|
||||||
output = StepOutput(success=True)
|
output = StepOutput(success=True)
|
||||||
assert output.error_message is None
|
assert output.error_message is None
|
||||||
@@ -90,31 +90,31 @@ class TestStepStatus:
|
|||||||
|
|
||||||
def test_step_status_has_pending(self):
|
def test_step_status_has_pending(self):
|
||||||
"""StepStatus 包含 PENDING 状态"""
|
"""StepStatus 包含 PENDING 状态"""
|
||||||
from kb_pipeline.schemas import StepStatus
|
from kb_workflow.pipeline.schemas import StepStatus
|
||||||
|
|
||||||
assert StepStatus.PENDING.value == "pending"
|
assert StepStatus.PENDING.value == "pending"
|
||||||
|
|
||||||
def test_step_status_has_running(self):
|
def test_step_status_has_running(self):
|
||||||
"""StepStatus 包含 RUNNING 状态"""
|
"""StepStatus 包含 RUNNING 状态"""
|
||||||
from kb_pipeline.schemas import StepStatus
|
from kb_workflow.pipeline.schemas import StepStatus
|
||||||
|
|
||||||
assert StepStatus.RUNNING.value == "running"
|
assert StepStatus.RUNNING.value == "running"
|
||||||
|
|
||||||
def test_step_status_has_completed(self):
|
def test_step_status_has_completed(self):
|
||||||
"""StepStatus 包含 COMPLETED 状态"""
|
"""StepStatus 包含 COMPLETED 状态"""
|
||||||
from kb_pipeline.schemas import StepStatus
|
from kb_workflow.pipeline.schemas import StepStatus
|
||||||
|
|
||||||
assert StepStatus.COMPLETED.value == "completed"
|
assert StepStatus.COMPLETED.value == "completed"
|
||||||
|
|
||||||
def test_step_status_has_failed(self):
|
def test_step_status_has_failed(self):
|
||||||
"""StepStatus 包含 FAILED 状态"""
|
"""StepStatus 包含 FAILED 状态"""
|
||||||
from kb_pipeline.schemas import StepStatus
|
from kb_workflow.pipeline.schemas import StepStatus
|
||||||
|
|
||||||
assert StepStatus.FAILED.value == "failed"
|
assert StepStatus.FAILED.value == "failed"
|
||||||
|
|
||||||
def test_step_status_has_skipped(self):
|
def test_step_status_has_skipped(self):
|
||||||
"""StepStatus 包含 SKIPPED 状态"""
|
"""StepStatus 包含 SKIPPED 状态"""
|
||||||
from kb_pipeline.schemas import StepStatus
|
from kb_workflow.pipeline.schemas import StepStatus
|
||||||
|
|
||||||
assert StepStatus.SKIPPED.value == "skipped"
|
assert StepStatus.SKIPPED.value == "skipped"
|
||||||
|
|
||||||
@@ -124,21 +124,21 @@ class TestStepRecord:
|
|||||||
|
|
||||||
def test_step_record_requires_step_name(self):
|
def test_step_record_requires_step_name(self):
|
||||||
"""StepRecord 必须包含 step_name"""
|
"""StepRecord 必须包含 step_name"""
|
||||||
from kb_pipeline.schemas import StepRecord, StepStatus
|
from kb_workflow.pipeline.schemas import StepRecord, StepStatus
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
StepRecord(status=StepStatus.COMPLETED)
|
StepRecord(status=StepStatus.COMPLETED)
|
||||||
|
|
||||||
def test_step_record_requires_status(self):
|
def test_step_record_requires_status(self):
|
||||||
"""StepRecord 必须包含 status"""
|
"""StepRecord 必须包含 status"""
|
||||||
from kb_pipeline.schemas import StepRecord
|
from kb_workflow.pipeline.schemas import StepRecord
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
StepRecord(step_name="fetch_metadata")
|
StepRecord(step_name="fetch_metadata")
|
||||||
|
|
||||||
def test_step_record_optional_fields(self):
|
def test_step_record_optional_fields(self):
|
||||||
"""StepRecord 可选字段正确工作"""
|
"""StepRecord 可选字段正确工作"""
|
||||||
from kb_pipeline.schemas import StepRecord, StepStatus
|
from kb_workflow.pipeline.schemas import StepRecord, StepStatus
|
||||||
|
|
||||||
record = StepRecord(step_name="fetch_metadata", status=StepStatus.COMPLETED)
|
record = StepRecord(step_name="fetch_metadata", status=StepStatus.COMPLETED)
|
||||||
assert record.started_at is None
|
assert record.started_at is None
|
||||||
@@ -152,28 +152,28 @@ class TestWorkflowState:
|
|||||||
|
|
||||||
def test_workflow_state_requires_content_id(self):
|
def test_workflow_state_requires_content_id(self):
|
||||||
"""WorkflowState 必须包含 content_id"""
|
"""WorkflowState 必须包含 content_id"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
WorkflowState(platform="bilibili", source_url="https://test.com")
|
WorkflowState(platform="bilibili", source_url="https://test.com")
|
||||||
|
|
||||||
def test_workflow_state_requires_platform(self):
|
def test_workflow_state_requires_platform(self):
|
||||||
"""WorkflowState 必须包含 platform"""
|
"""WorkflowState 必须包含 platform"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
WorkflowState(content_id="BV1test", source_url="https://test.com")
|
WorkflowState(content_id="BV1test", source_url="https://test.com")
|
||||||
|
|
||||||
def test_workflow_state_requires_source_url(self):
|
def test_workflow_state_requires_source_url(self):
|
||||||
"""WorkflowState 必须包含 source_url"""
|
"""WorkflowState 必须包含 source_url"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
WorkflowState(content_id="BV1test", platform="bilibili")
|
WorkflowState(content_id="BV1test", platform="bilibili")
|
||||||
|
|
||||||
def test_workflow_state_has_steps_dict(self):
|
def test_workflow_state_has_steps_dict(self):
|
||||||
"""WorkflowState 包含 steps 字典"""
|
"""WorkflowState 包含 steps 字典"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -185,7 +185,7 @@ class TestWorkflowState:
|
|||||||
|
|
||||||
def test_workflow_state_default_schema_version(self):
|
def test_workflow_state_default_schema_version(self):
|
||||||
"""WorkflowState schema_version 默认为 1.0"""
|
"""WorkflowState schema_version 默认为 1.0"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -196,7 +196,7 @@ class TestWorkflowState:
|
|||||||
|
|
||||||
def test_workflow_state_is_step_completed_method(self):
|
def test_workflow_state_is_step_completed_method(self):
|
||||||
"""WorkflowState 有 is_step_completed 方法"""
|
"""WorkflowState 有 is_step_completed 方法"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepStatus, StepRecord
|
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus, StepRecord
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -220,7 +220,7 @@ class TestWorkflowStateMethods:
|
|||||||
|
|
||||||
def test_mark_step_started(self):
|
def test_mark_step_started(self):
|
||||||
"""mark_step_started 正确设置步骤状态"""
|
"""mark_step_started 正确设置步骤状态"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -236,7 +236,7 @@ class TestWorkflowStateMethods:
|
|||||||
|
|
||||||
def test_mark_step_completed_existing_step(self):
|
def test_mark_step_completed_existing_step(self):
|
||||||
"""mark_step_completed 更新已存在的步骤"""
|
"""mark_step_completed 更新已存在的步骤"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -253,7 +253,7 @@ class TestWorkflowStateMethods:
|
|||||||
|
|
||||||
def test_mark_step_completed_new_step(self):
|
def test_mark_step_completed_new_step(self):
|
||||||
"""mark_step_completed 创建新步骤记录"""
|
"""mark_step_completed 创建新步骤记录"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -269,7 +269,7 @@ class TestWorkflowStateMethods:
|
|||||||
|
|
||||||
def test_mark_step_failed_existing_step(self):
|
def test_mark_step_failed_existing_step(self):
|
||||||
"""mark_step_failed 更新已存在的步骤为失败"""
|
"""mark_step_failed 更新已存在的步骤为失败"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -286,7 +286,7 @@ class TestWorkflowStateMethods:
|
|||||||
|
|
||||||
def test_mark_step_failed_new_step(self):
|
def test_mark_step_failed_new_step(self):
|
||||||
"""mark_step_failed 创建新的失败步骤记录"""
|
"""mark_step_failed 创建新的失败步骤记录"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
content_id="BV1test",
|
content_id="BV1test",
|
||||||
@@ -306,7 +306,7 @@ class TestStepResult:
|
|||||||
|
|
||||||
def test_step_result_creation(self):
|
def test_step_result_creation(self):
|
||||||
"""StepResult 正确创建"""
|
"""StepResult 正确创建"""
|
||||||
from kb_pipeline.schemas import StepResult, StepInput, StepStatus
|
from kb_workflow.pipeline.schemas import StepResult, StepInput, StepStatus
|
||||||
|
|
||||||
input_data = StepInput(content_id="BV1test", platform="bilibili")
|
input_data = StepInput(content_id="BV1test", platform="bilibili")
|
||||||
result = StepResult(step_name="fetch_metadata", input_data=input_data)
|
result = StepResult(step_name="fetch_metadata", input_data=input_data)
|
||||||
@@ -317,7 +317,7 @@ class TestStepResult:
|
|||||||
|
|
||||||
def test_step_result_with_output(self):
|
def test_step_result_with_output(self):
|
||||||
"""StepResult 包含输出数据"""
|
"""StepResult 包含输出数据"""
|
||||||
from kb_pipeline.schemas import StepResult, StepInput, StepOutput, StepStatus
|
from kb_workflow.pipeline.schemas import StepResult, StepInput, StepOutput, StepStatus
|
||||||
|
|
||||||
input_data = StepInput(content_id="BV1test", platform="bilibili")
|
input_data = StepInput(content_id="BV1test", platform="bilibili")
|
||||||
output_data = StepOutput(success=True)
|
output_data = StepOutput(success=True)
|
||||||
@@ -337,7 +337,7 @@ class TestStepDefinition:
|
|||||||
|
|
||||||
def test_step_definition_creation(self):
|
def test_step_definition_creation(self):
|
||||||
"""StepDefinition 正确创建"""
|
"""StepDefinition 正确创建"""
|
||||||
from kb_pipeline.schemas import StepDefinition, StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepDefinition, StepInput, StepOutput
|
||||||
|
|
||||||
definition = StepDefinition(
|
definition = StepDefinition(
|
||||||
step_name="fetch_metadata",
|
step_name="fetch_metadata",
|
||||||
@@ -352,7 +352,7 @@ class TestStepDefinition:
|
|||||||
|
|
||||||
def test_step_definition_with_dependencies(self):
|
def test_step_definition_with_dependencies(self):
|
||||||
"""StepDefinition 包含依赖"""
|
"""StepDefinition 包含依赖"""
|
||||||
from kb_pipeline.schemas import StepDefinition, StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepDefinition, StepInput, StepOutput
|
||||||
|
|
||||||
definition = StepDefinition(
|
definition = StepDefinition(
|
||||||
step_name="transcribe_audio",
|
step_name="transcribe_audio",
|
||||||
@@ -371,14 +371,14 @@ class TestSchemaValidation:
|
|||||||
|
|
||||||
def test_validation_error_on_invalid_input(self):
|
def test_validation_error_on_invalid_input(self):
|
||||||
"""无效输入抛出 ValidationError"""
|
"""无效输入抛出 ValidationError"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
StepInput(content_id=123, platform="bilibili") # type: ignore
|
StepInput(content_id=123, platform="bilibili") # type: ignore
|
||||||
|
|
||||||
def test_model_validate_from_dict(self):
|
def test_model_validate_from_dict(self):
|
||||||
"""model_validate 从字典正确创建实例"""
|
"""model_validate 从字典正确创建实例"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
|
|
||||||
data = {"content_id": "BV1test", "platform": "bilibili"}
|
data = {"content_id": "BV1test", "platform": "bilibili"}
|
||||||
step_input = StepInput.model_validate(data)
|
step_input = StepInput.model_validate(data)
|
||||||
@@ -391,7 +391,7 @@ class TestJSONSerialization:
|
|||||||
|
|
||||||
def test_step_input_model_dump_json(self):
|
def test_step_input_model_dump_json(self):
|
||||||
"""StepInput 支持 model_dump_json"""
|
"""StepInput 支持 model_dump_json"""
|
||||||
from kb_pipeline.schemas import StepInput
|
from kb_workflow.pipeline.schemas import StepInput
|
||||||
import json
|
import json
|
||||||
|
|
||||||
step_input = StepInput(content_id="BV1test", platform="bilibili")
|
step_input = StepInput(content_id="BV1test", platform="bilibili")
|
||||||
@@ -403,7 +403,7 @@ class TestJSONSerialization:
|
|||||||
|
|
||||||
def test_step_output_model_dump_json(self):
|
def test_step_output_model_dump_json(self):
|
||||||
"""StepOutput 支持 model_dump_json"""
|
"""StepOutput 支持 model_dump_json"""
|
||||||
from kb_pipeline.schemas import StepOutput
|
from kb_workflow.pipeline.schemas import StepOutput
|
||||||
import json
|
import json
|
||||||
|
|
||||||
output = StepOutput(success=True, error_message=None)
|
output = StepOutput(success=True, error_message=None)
|
||||||
@@ -415,7 +415,7 @@ class TestJSONSerialization:
|
|||||||
|
|
||||||
def test_workflow_state_model_dump_json(self):
|
def test_workflow_state_model_dump_json(self):
|
||||||
"""WorkflowState 支持 model_dump_json"""
|
"""WorkflowState 支持 model_dump_json"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
import json
|
import json
|
||||||
|
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
@@ -433,7 +433,7 @@ class TestJSONSerialization:
|
|||||||
|
|
||||||
def test_workflow_state_model_validate_from_json(self):
|
def test_workflow_state_model_validate_from_json(self):
|
||||||
"""WorkflowState 可从 JSON 恢复"""
|
"""WorkflowState 可从 JSON 恢复"""
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
import json
|
import json
|
||||||
|
|
||||||
original = WorkflowState(
|
original = WorkflowState(
|
||||||
@@ -450,7 +450,7 @@ class TestJSONSerialization:
|
|||||||
|
|
||||||
def test_workflow_state_with_steps_serialization(self):
|
def test_workflow_state_with_steps_serialization(self):
|
||||||
"""WorkflowState steps 字典正确序列化/反序列化"""
|
"""WorkflowState steps 字典正确序列化/反序列化"""
|
||||||
from kb_pipeline.schemas import WorkflowState, StepRecord, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepRecord, StepStatus
|
||||||
import json
|
import json
|
||||||
|
|
||||||
original = WorkflowState(
|
original = WorkflowState(
|
||||||
@@ -484,7 +484,7 @@ class TestParseInput:
|
|||||||
|
|
||||||
def test_parse_input_validates_url_field(self):
|
def test_parse_input_validates_url_field(self):
|
||||||
"""ParseInput 必须包含 url 字段,且 min_length=1"""
|
"""ParseInput 必须包含 url 字段,且 min_length=1"""
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
# 缺少 url
|
# 缺少 url
|
||||||
with pytest.raises(ValidationError) as exc_info:
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
@@ -501,14 +501,14 @@ class TestParseInput:
|
|||||||
|
|
||||||
def test_parse_input_content_id_default_empty(self):
|
def test_parse_input_content_id_default_empty(self):
|
||||||
"""ParseInput content_id 默认为空字符串"""
|
"""ParseInput content_id 默认为空字符串"""
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
input_data = ParseInput(url="https://www.bilibili.com/video/BV1xx")
|
input_data = ParseInput(url="https://www.bilibili.com/video/BV1xx")
|
||||||
assert input_data.content_id == ""
|
assert input_data.content_id == ""
|
||||||
|
|
||||||
def test_parse_input_platform_default_empty(self):
|
def test_parse_input_platform_default_empty(self):
|
||||||
"""ParseInput platform 默认为空字符串"""
|
"""ParseInput platform 默认为空字符串"""
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
input_data = ParseInput(url="https://www.bilibili.com/video/BV1xx")
|
input_data = ParseInput(url="https://www.bilibili.com/video/BV1xx")
|
||||||
assert input_data.platform == ""
|
assert input_data.platform == ""
|
||||||
@@ -519,7 +519,7 @@ class TestParseOutput:
|
|||||||
|
|
||||||
def test_parse_output_inherits_step_output(self):
|
def test_parse_output_inherits_step_output(self):
|
||||||
"""ParseOutput 继承 StepOutput 的 success 和 error_message"""
|
"""ParseOutput 继承 StepOutput 的 success 和 error_message"""
|
||||||
from kb_pipeline.schemas import ParseOutput, StepOutput
|
from kb_workflow.pipeline.schemas import ParseOutput, StepOutput
|
||||||
|
|
||||||
output = ParseOutput(
|
output = ParseOutput(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -536,7 +536,7 @@ class TestParseOutput:
|
|||||||
|
|
||||||
def test_parse_output_has_required_fields(self):
|
def test_parse_output_has_required_fields(self):
|
||||||
"""ParseOutput 包含所有必需字段"""
|
"""ParseOutput 包含所有必需字段"""
|
||||||
from kb_pipeline.schemas import ParseOutput
|
from kb_workflow.pipeline.schemas import ParseOutput
|
||||||
|
|
||||||
output = ParseOutput(
|
output = ParseOutput(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -555,7 +555,7 @@ class TestParseOutput:
|
|||||||
|
|
||||||
def test_parse_output_with_error(self):
|
def test_parse_output_with_error(self):
|
||||||
"""ParseOutput 可以表示失败状态"""
|
"""ParseOutput 可以表示失败状态"""
|
||||||
from kb_pipeline.schemas import ParseOutput
|
from kb_workflow.pipeline.schemas import ParseOutput
|
||||||
|
|
||||||
output = ParseOutput(
|
output = ParseOutput(
|
||||||
success=False,
|
success=False,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import pytest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from kb_pipeline.schemas import WorkflowState, StepRecord, StepStatus
|
from kb_workflow.pipeline.schemas import WorkflowState, StepRecord, StepStatus
|
||||||
|
|
||||||
|
|
||||||
class TestWorkflowStateIsStepCompleted:
|
class TestWorkflowStateIsStepCompleted:
|
||||||
@@ -157,7 +157,7 @@ class TestAtomicWrite:
|
|||||||
|
|
||||||
def test_save_creates_state_file(self, tmp_path):
|
def test_save_creates_state_file(self, tmp_path):
|
||||||
"""save creates workflow_state.json in content directory."""
|
"""save creates workflow_state.json in content directory."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
@@ -173,7 +173,7 @@ class TestAtomicWrite:
|
|||||||
|
|
||||||
def test_save_creates_parent_directories(self, tmp_path):
|
def test_save_creates_parent_directories(self, tmp_path):
|
||||||
"""save creates parent directories if they don't exist."""
|
"""save creates parent directories if they don't exist."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
nested_dir = tmp_path / "deeply" / "nested" / "content"
|
nested_dir = tmp_path / "deeply" / "nested" / "content"
|
||||||
manager = WorkflowStateManager(content_dir=nested_dir)
|
manager = WorkflowStateManager(content_dir=nested_dir)
|
||||||
@@ -190,7 +190,7 @@ class TestAtomicWrite:
|
|||||||
|
|
||||||
def test_save_rotates_backup(self, tmp_path):
|
def test_save_rotates_backup(self, tmp_path):
|
||||||
"""save rotates backup (workflow_state.json -> workflow_state.json.bak)."""
|
"""save rotates backup (workflow_state.json -> workflow_state.json.bak)."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
state1 = WorkflowState(
|
state1 = WorkflowState(
|
||||||
@@ -228,7 +228,7 @@ class TestAtomicWrite:
|
|||||||
|
|
||||||
def test_no_temp_files_remain_after_save(self, tmp_path):
|
def test_no_temp_files_remain_after_save(self, tmp_path):
|
||||||
"""No temp files remain after successful save."""
|
"""No temp files remain after successful save."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
state = WorkflowState(
|
state = WorkflowState(
|
||||||
@@ -248,7 +248,7 @@ class TestBackupFallback:
|
|||||||
|
|
||||||
def test_load_returns_state_for_valid_file(self, tmp_path):
|
def test_load_returns_state_for_valid_file(self, tmp_path):
|
||||||
"""load returns WorkflowState for valid file."""
|
"""load returns WorkflowState for valid file."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
original = WorkflowState(
|
original = WorkflowState(
|
||||||
@@ -266,7 +266,7 @@ class TestBackupFallback:
|
|||||||
|
|
||||||
def test_load_returns_none_for_missing_file(self, tmp_path):
|
def test_load_returns_none_for_missing_file(self, tmp_path):
|
||||||
"""load returns None when no state file exists."""
|
"""load returns None when no state file exists."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
loaded = manager.load()
|
loaded = manager.load()
|
||||||
@@ -275,7 +275,7 @@ class TestBackupFallback:
|
|||||||
|
|
||||||
def test_load_falls_back_to_backup_on_corruption(self, tmp_path):
|
def test_load_falls_back_to_backup_on_corruption(self, tmp_path):
|
||||||
"""load falls back to backup when main file is corrupted."""
|
"""load falls back to backup when main file is corrupted."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
state_file = tmp_path / "workflow_state.json"
|
state_file = tmp_path / "workflow_state.json"
|
||||||
@@ -312,7 +312,7 @@ class TestBackupFallback:
|
|||||||
|
|
||||||
def test_load_from_backup_when_main_missing(self, tmp_path):
|
def test_load_from_backup_when_main_missing(self, tmp_path):
|
||||||
"""load loads from backup when main file is missing."""
|
"""load loads from backup when main file is missing."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
state_file = tmp_path / "workflow_state.json"
|
state_file = tmp_path / "workflow_state.json"
|
||||||
@@ -347,7 +347,7 @@ class TestStateManagerGetOrCreate:
|
|||||||
|
|
||||||
def test_get_or_create_returns_existing_state(self, tmp_path):
|
def test_get_or_create_returns_existing_state(self, tmp_path):
|
||||||
"""get_or_create returns existing state when it exists."""
|
"""get_or_create returns existing state when it exists."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ class TestStateManagerGetOrCreate:
|
|||||||
|
|
||||||
def test_get_or_create_creates_new_state(self, tmp_path):
|
def test_get_or_create_creates_new_state(self, tmp_path):
|
||||||
"""get_or_create creates new state when it doesn't exist."""
|
"""get_or_create creates new state when it doesn't exist."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ class TestStateManagerGetOrCreate:
|
|||||||
|
|
||||||
def test_get_or_create_is_idempotent(self, tmp_path):
|
def test_get_or_create_is_idempotent(self, tmp_path):
|
||||||
"""get_or_create is idempotent - calling twice returns same state."""
|
"""get_or_create is idempotent - calling twice returns same state."""
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
manager = WorkflowStateManager(content_dir=tmp_path)
|
manager = WorkflowStateManager(content_dir=tmp_path)
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from pathlib import Path
|
|||||||
import tempfile
|
import tempfile
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
from kb_pipeline.schemas import WorkflowState
|
from kb_workflow.pipeline.schemas import WorkflowState
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import pytest
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from kb_pipeline.platforms.base import PlatformAdapter
|
from kb_workflow.pipeline.platforms.base import PlatformAdapter
|
||||||
|
|
||||||
|
|
||||||
class TestPlatformAdapterInterface:
|
class TestPlatformAdapterInterface:
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import pytest
|
|||||||
from unittest.mock import MagicMock, AsyncMock, patch
|
from unittest.mock import MagicMock, AsyncMock, patch
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from kb_pipeline.platforms.bilibili import BilibiliAdapter
|
from kb_workflow.pipeline.platforms.bilibili import BilibiliAdapter
|
||||||
from kb_pipeline.errors import NetworkError, AuthError
|
from kb_workflow.pipeline.errors import NetworkError, AuthError
|
||||||
|
|
||||||
|
|
||||||
class TestBilibiliAdapterContentType:
|
class TestBilibiliAdapterContentType:
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import pytest
|
|||||||
from unittest.mock import MagicMock, patch, PropertyMock
|
from unittest.mock import MagicMock, patch, PropertyMock
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from kb_pipeline.platforms.zhihu import ZhihuAdapter
|
from kb_workflow.pipeline.platforms.zhihu import ZhihuAdapter
|
||||||
from kb_pipeline.errors import AuthError
|
from kb_workflow.pipeline.errors import AuthError
|
||||||
|
|
||||||
|
|
||||||
class TestZhihuAdapterAnswerContent:
|
class TestZhihuAdapterAnswerContent:
|
||||||
@@ -168,7 +168,7 @@ class TestZhihuAdapterCookieHandling:
|
|||||||
|
|
||||||
def test_cookie_from_settings(self):
|
def test_cookie_from_settings(self):
|
||||||
"""Test cookie is loaded from settings"""
|
"""Test cookie is loaded from settings"""
|
||||||
with patch('kb_pipeline.platforms.zhihu.get_settings') as mock_settings:
|
with patch('kb_workflow.pipeline.platforms.zhihu.get_settings') as mock_settings:
|
||||||
mock_secret = MagicMock()
|
mock_secret = MagicMock()
|
||||||
mock_secret.get_secret_value.return_value = "test_cookie_value"
|
mock_secret.get_secret_value.return_value = "test_cookie_value"
|
||||||
mock_settings.return_value.zhihu_cookie = mock_secret
|
mock_settings.return_value.zhihu_cookie = mock_secret
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from pathlib import Path
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from kb_pipeline.schemas import AnalyzeInput, AnalyzeOutput
|
from kb_workflow.pipeline.schemas import AnalyzeInput, AnalyzeOutput
|
||||||
|
|
||||||
|
|
||||||
# Mock AnalysisResult for testing
|
# Mock AnalysisResult for testing
|
||||||
@@ -213,8 +213,8 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 1: AnalyzeProcessor loads metadata and content from files"""
|
"""Test 1: AnalyzeProcessor loads metadata and content from files"""
|
||||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
|
||||||
from kb_pipeline.schemas import AnalyzeInput
|
from kb_workflow.pipeline.schemas import AnalyzeInput
|
||||||
|
|
||||||
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
|
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
output_file=output_file
|
output_file=output_file
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||||
|
|
||||||
@@ -251,8 +251,8 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 2: AnalyzeProcessor calls DeepAnalyzer.analyze() with correct parameters"""
|
"""Test 2: AnalyzeProcessor calls DeepAnalyzer.analyze() with correct parameters"""
|
||||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
|
||||||
from kb_pipeline.schemas import AnalyzeInput
|
from kb_workflow.pipeline.schemas import AnalyzeInput
|
||||||
|
|
||||||
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
|
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
output_file=output_file
|
output_file=output_file
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||||
|
|
||||||
@@ -291,8 +291,8 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 3: AnalyzeProcessor saves analysis report as Markdown"""
|
"""Test 3: AnalyzeProcessor saves analysis report as Markdown"""
|
||||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
|
||||||
from kb_pipeline.schemas import AnalyzeInput
|
from kb_workflow.pipeline.schemas import AnalyzeInput
|
||||||
|
|
||||||
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
|
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
output_file=output_file
|
output_file=output_file
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||||
|
|
||||||
@@ -338,8 +338,8 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 4: AnalyzeProcessor records state on success"""
|
"""Test 4: AnalyzeProcessor records state on success"""
|
||||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
|
||||||
from kb_pipeline.schemas import AnalyzeInput, WorkflowState
|
from kb_workflow.pipeline.schemas import AnalyzeInput, WorkflowState
|
||||||
|
|
||||||
# Create a mock state that will be updated
|
# Create a mock state that will be updated
|
||||||
mock_state = MagicMock(spec=WorkflowState)
|
mock_state = MagicMock(spec=WorkflowState)
|
||||||
@@ -357,7 +357,7 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
output_file=output_file
|
output_file=output_file
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||||
|
|
||||||
@@ -381,8 +381,8 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
self, mock_state_manager, sample_metadata, sample_content, tmp_path
|
self, mock_state_manager, sample_metadata, sample_content, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 5: AnalyzeProcessor records state on failure"""
|
"""Test 5: AnalyzeProcessor records state on failure"""
|
||||||
from kb_pipeline.processors.analyze import AnalyzeProcessor
|
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
|
||||||
from kb_pipeline.schemas import AnalyzeInput, WorkflowState
|
from kb_workflow.pipeline.schemas import AnalyzeInput, WorkflowState
|
||||||
|
|
||||||
# Create a mock state that will be updated
|
# Create a mock state that will be updated
|
||||||
mock_state = MagicMock(spec=WorkflowState)
|
mock_state = MagicMock(spec=WorkflowState)
|
||||||
@@ -400,7 +400,7 @@ class TestAnalyzeProcessorIntegration:
|
|||||||
output_file=output_file
|
output_file=output_file
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
processor = AnalyzeProcessor(state_manager=mock_state_manager)
|
||||||
|
|
||||||
@@ -424,8 +424,8 @@ class TestAnalyzeProcessorModuleExport:
|
|||||||
"""Tests for AnalyzeProcessor module export."""
|
"""Tests for AnalyzeProcessor module export."""
|
||||||
|
|
||||||
def test_analyze_processor_importable_from_processors(self):
|
def test_analyze_processor_importable_from_processors(self):
|
||||||
"""Test: AnalyzeProcessor can be imported from kb_pipeline.processors"""
|
"""Test: AnalyzeProcessor can be imported from kb_workflow.pipeline.processors"""
|
||||||
from kb_pipeline.processors import AnalyzeProcessor
|
from kb_workflow.pipeline.processors import AnalyzeProcessor
|
||||||
|
|
||||||
assert AnalyzeProcessor is not None
|
assert AnalyzeProcessor is not None
|
||||||
assert hasattr(AnalyzeProcessor, 'STEP_NAME')
|
assert hasattr(AnalyzeProcessor, 'STEP_NAME')
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from kb_pipeline.processors.archive import ArchiveProcessor
|
from kb_workflow.pipeline.processors.archive import ArchiveProcessor
|
||||||
from kb_pipeline.schemas import ArchiveInput, ArchiveOutput
|
from kb_workflow.pipeline.schemas import ArchiveInput, ArchiveOutput
|
||||||
from kb_pipeline.state import WorkflowStateManager
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -63,7 +63,7 @@ class TestArchiveProcessorCopyToPlatformDir:
|
|||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
mock_settings.archive_path = str(archive_test_env["archive_dir"])
|
mock_settings.archive_path = str(archive_test_env["archive_dir"])
|
||||||
|
|
||||||
with patch('kb_pipeline.processors.archive.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.archive.get_settings') as mock_get_settings:
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
|
|
||||||
processor = ArchiveProcessor()
|
processor = ArchiveProcessor()
|
||||||
@@ -95,7 +95,7 @@ class TestArchiveProcessorFilenameFormat:
|
|||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
mock_settings.archive_path = str(archive_test_env["archive_dir"])
|
mock_settings.archive_path = str(archive_test_env["archive_dir"])
|
||||||
|
|
||||||
with patch('kb_pipeline.processors.archive.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.archive.get_settings') as mock_get_settings:
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
|
|
||||||
processor = ArchiveProcessor()
|
processor = ArchiveProcessor()
|
||||||
@@ -138,7 +138,7 @@ class TestArchiveProcessorSkipsCompleted:
|
|||||||
state.mark_step_completed("archive")
|
state.mark_step_completed("archive")
|
||||||
archive_test_env["state_manager"].save(state)
|
archive_test_env["state_manager"].save(state)
|
||||||
|
|
||||||
with patch('kb_pipeline.processors.archive.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.archive.get_settings') as mock_get_settings:
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
|
|
||||||
processor = ArchiveProcessor(state_manager=archive_test_env["state_manager"])
|
processor = ArchiveProcessor(state_manager=archive_test_env["state_manager"])
|
||||||
@@ -171,7 +171,7 @@ class TestArchiveProcessorDetectsExisting:
|
|||||||
existing = platform_dir / "20260101_BV1test_analysis.md"
|
existing = platform_dir / "20260101_BV1test_analysis.md"
|
||||||
existing.write_text("Old content")
|
existing.write_text("Old content")
|
||||||
|
|
||||||
with patch('kb_pipeline.processors.archive.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.archive.get_settings') as mock_get_settings:
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
|
|
||||||
processor = ArchiveProcessor()
|
processor = ArchiveProcessor()
|
||||||
@@ -201,7 +201,7 @@ class TestArchiveProcessorHandlesMissingFile:
|
|||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
mock_settings.archive_path = str(archive_test_env["archive_dir"])
|
mock_settings.archive_path = str(archive_test_env["archive_dir"])
|
||||||
|
|
||||||
with patch('kb_pipeline.processors.archive.get_settings') as mock_get_settings:
|
with patch('kb_workflow.pipeline.processors.archive.get_settings') as mock_get_settings:
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
|
|
||||||
processor = ArchiveProcessor()
|
processor = ArchiveProcessor()
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ from unittest.mock import AsyncMock, MagicMock, patch, mock_open
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
# 测试将在实现后导入
|
# 测试将在实现后导入
|
||||||
# from kb_pipeline.schemas import ASRInput, ASROutput
|
# from kb_workflow.pipeline.schemas import ASRInput, ASROutput
|
||||||
# from kb_pipeline.processors.asr import ASRProcessor
|
# from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -25,7 +25,7 @@ class TestASRInputSchema:
|
|||||||
|
|
||||||
def test_asr_input_inherits_step_input(self):
|
def test_asr_input_inherits_step_input(self):
|
||||||
"""ASRInput 应继承 StepInput 的字段"""
|
"""ASRInput 应继承 StepInput 的字段"""
|
||||||
from kb_pipeline.schemas import ASRInput, StepInput
|
from kb_workflow.pipeline.schemas import ASRInput, StepInput
|
||||||
|
|
||||||
# 创建实例
|
# 创建实例
|
||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
@@ -42,7 +42,7 @@ class TestASRInputSchema:
|
|||||||
|
|
||||||
def test_asr_input_has_video_path(self):
|
def test_asr_input_has_video_path(self):
|
||||||
"""ASRInput 必须包含 video_path 字段"""
|
"""ASRInput 必须包含 video_path 字段"""
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
@@ -56,7 +56,7 @@ class TestASRInputSchema:
|
|||||||
|
|
||||||
def test_asr_input_content_type_default(self):
|
def test_asr_input_content_type_default(self):
|
||||||
"""ASRInput content_type 默认为 'video'"""
|
"""ASRInput content_type 默认为 'video'"""
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
@@ -73,7 +73,7 @@ class TestASROutputSchema:
|
|||||||
|
|
||||||
def test_asr_output_inherits_step_output(self):
|
def test_asr_output_inherits_step_output(self):
|
||||||
"""ASROutput 应继承 StepOutput 的字段"""
|
"""ASROutput 应继承 StepOutput 的字段"""
|
||||||
from kb_pipeline.schemas import ASROutput, StepOutput
|
from kb_workflow.pipeline.schemas import ASROutput, StepOutput
|
||||||
|
|
||||||
output = ASROutput(
|
output = ASROutput(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -90,7 +90,7 @@ class TestASROutputSchema:
|
|||||||
|
|
||||||
def test_asr_output_tool_literal(self):
|
def test_asr_output_tool_literal(self):
|
||||||
"""ASROutput tool 字段只能是 'subtitle' | 'whisper' | 'whisper-diarization'"""
|
"""ASROutput tool 字段只能是 'subtitle' | 'whisper' | 'whisper-diarization'"""
|
||||||
from kb_pipeline.schemas import ASROutput
|
from kb_workflow.pipeline.schemas import ASROutput
|
||||||
|
|
||||||
# 有效值
|
# 有效值
|
||||||
for tool in ["subtitle", "whisper", "whisper-diarization"]:
|
for tool in ["subtitle", "whisper", "whisper-diarization"]:
|
||||||
@@ -107,7 +107,7 @@ class TestASROutputSchema:
|
|||||||
|
|
||||||
def test_asr_output_tool_invalid(self):
|
def test_asr_output_tool_invalid(self):
|
||||||
"""ASROutput tool 字段无效值应抛出 ValidationError"""
|
"""ASROutput tool 字段无效值应抛出 ValidationError"""
|
||||||
from kb_pipeline.schemas import ASROutput
|
from kb_workflow.pipeline.schemas import ASROutput
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
ASROutput(
|
ASROutput(
|
||||||
@@ -122,7 +122,7 @@ class TestASROutputSchema:
|
|||||||
|
|
||||||
def test_asr_output_has_required_fields(self):
|
def test_asr_output_has_required_fields(self):
|
||||||
"""ASROutput 必须包含所有必需字段"""
|
"""ASROutput 必须包含所有必需字段"""
|
||||||
from kb_pipeline.schemas import ASROutput
|
from kb_workflow.pipeline.schemas import ASROutput
|
||||||
|
|
||||||
output = ASROutput(
|
output = ASROutput(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -143,7 +143,7 @@ class TestASROutputSchema:
|
|||||||
|
|
||||||
def test_asr_output_speakers_optional(self):
|
def test_asr_output_speakers_optional(self):
|
||||||
"""ASROutput speakers 字段是可选的"""
|
"""ASROutput speakers 字段是可选的"""
|
||||||
from kb_pipeline.schemas import ASROutput
|
from kb_workflow.pipeline.schemas import ASROutput
|
||||||
|
|
||||||
# 无 speakers
|
# 无 speakers
|
||||||
output1 = ASROutput(
|
output1 = ASROutput(
|
||||||
@@ -180,18 +180,18 @@ class TestASRProcessorInit:
|
|||||||
|
|
||||||
def test_asr_processor_init_success(self, mock_settings):
|
def test_asr_processor_init_success(self, mock_settings):
|
||||||
"""ASRProcessor 初始化成功"""
|
"""ASRProcessor 初始化成功"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
assert processor is not None
|
assert processor is not None
|
||||||
assert processor.STEP_NAME == "asr"
|
assert processor.STEP_NAME == "asr"
|
||||||
|
|
||||||
def test_asr_processor_with_state_manager(self, mock_settings, mock_state_manager):
|
def test_asr_processor_with_state_manager(self, mock_settings, mock_state_manager):
|
||||||
"""ASRProcessor 可接受 state_manager 参数"""
|
"""ASRProcessor 可接受 state_manager 参数"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor(state_manager=mock_state_manager)
|
processor = ASRProcessor(state_manager=mock_state_manager)
|
||||||
assert processor.state_manager is mock_state_manager
|
assert processor.state_manager is mock_state_manager
|
||||||
|
|
||||||
@@ -202,13 +202,13 @@ class TestOfficialSubtitle:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_official_subtitle_found(self, mock_settings, sample_subtitle_response, sample_subtitle_content, tmp_path):
|
async def test_official_subtitle_found(self, mock_settings, sample_subtitle_response, sample_subtitle_content, tmp_path):
|
||||||
"""_try_official_subtitle 找到字幕时返回 ASROutput"""
|
"""_try_official_subtitle 找到字幕时返回 ASROutput"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
# Mock API 调用
|
# Mock API 调用
|
||||||
@@ -238,13 +238,13 @@ class TestOfficialSubtitle:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_official_subtitle_not_found(self, mock_settings, tmp_path):
|
async def test_official_subtitle_not_found(self, mock_settings, tmp_path):
|
||||||
"""_try_official_subtitle 无字幕时返回 None"""
|
"""_try_official_subtitle 无字幕时返回 None"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
# Mock API 调用 - 无字幕
|
# Mock API 调用 - 无字幕
|
||||||
@@ -272,13 +272,13 @@ class TestOfficialSubtitle:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_process_tries_subtitle_first(self, mock_settings, tmp_path):
|
async def test_process_tries_subtitle_first(self, mock_settings, tmp_path):
|
||||||
"""process() 方法优先尝试官方字幕"""
|
"""process() 方法优先尝试官方字幕"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
# Mock _try_official_subtitle 返回成功
|
# Mock _try_official_subtitle 返回成功
|
||||||
@@ -304,8 +304,8 @@ class TestWhisperBasic:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_whisper_basic(self, mock_settings, mock_whisper_path, tmp_path):
|
async def test_whisper_basic(self, mock_settings, mock_whisper_path, tmp_path):
|
||||||
"""_run_whisper 调用成功返回 ASROutput"""
|
"""_run_whisper 调用成功返回 ASROutput"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
@@ -318,14 +318,14 @@ class TestWhisperBasic:
|
|||||||
srt_path.write_text("1\n00:00:00,000 --> 00:00:03,000\ntest\n")
|
srt_path.write_text("1\n00:00:00,000 --> 00:00:03,000\ntest\n")
|
||||||
json_path.write_text('{"transcription": [{"text": "test"}]}')
|
json_path.write_text('{"transcription": [{"text": "test"}]}')
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_whisper_cpp_path", return_value=mock_whisper_path):
|
with patch("kb_workflow.pipeline.processors.asr.get_whisper_cpp_path", return_value=mock_whisper_path):
|
||||||
with patch("subprocess.run") as mock_run:
|
with patch("subprocess.run") as mock_run:
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.convert_to_wav") as mock_convert:
|
with patch("kb_workflow.pipeline.processors.asr.convert_to_wav") as mock_convert:
|
||||||
mock_convert.return_value = str(tmp_path / "test_audio.wav")
|
mock_convert.return_value = str(tmp_path / "test_audio.wav")
|
||||||
|
|
||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
@@ -344,8 +344,8 @@ class TestWhisperBasic:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_temp_file_cleanup(self, mock_settings, mock_whisper_path, tmp_path):
|
async def test_temp_file_cleanup(self, mock_settings, mock_whisper_path, tmp_path):
|
||||||
"""临时音频文件处理后自动清理"""
|
"""临时音频文件处理后自动清理"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
@@ -353,14 +353,14 @@ class TestWhisperBasic:
|
|||||||
audio_path = tmp_path / "test_audio.wav"
|
audio_path = tmp_path / "test_audio.wav"
|
||||||
audio_path.write_text("fake audio")
|
audio_path.write_text("fake audio")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_whisper_cpp_path", return_value=mock_whisper_path):
|
with patch("kb_workflow.pipeline.processors.asr.get_whisper_cpp_path", return_value=mock_whisper_path):
|
||||||
with patch("subprocess.run") as mock_run:
|
with patch("subprocess.run") as mock_run:
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.convert_to_wav", return_value=str(audio_path)):
|
with patch("kb_workflow.pipeline.processors.asr.convert_to_wav", return_value=str(audio_path)):
|
||||||
with patch.object(processor, "_parse_whisper_output") as mock_parse:
|
with patch.object(processor, "_parse_whisper_output") as mock_parse:
|
||||||
mock_parse.return_value = MagicMock(
|
mock_parse.return_value = MagicMock(
|
||||||
success=True,
|
success=True,
|
||||||
@@ -387,16 +387,16 @@ class TestWhisperBasic:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_whisper_path_not_found(self, mock_settings, tmp_path):
|
async def test_whisper_path_not_found(self, mock_settings, tmp_path):
|
||||||
"""Whisper 路径不存在时抛出 RuntimeError"""
|
"""Whisper 路径不存在时抛出 RuntimeError"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor, WhisperNotFoundError
|
from kb_workflow.pipeline.processors.asr import ASRProcessor, WhisperNotFoundError
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_whisper_cpp_path", side_effect=WhisperNotFoundError("Not found")):
|
with patch("kb_workflow.pipeline.processors.asr.get_whisper_cpp_path", side_effect=WhisperNotFoundError("Not found")):
|
||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
@@ -414,20 +414,20 @@ class TestDiarization:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_no_hf_token_uses_basic_whisper(self, mock_settings, mock_whisper_path, tmp_path):
|
async def test_no_hf_token_uses_basic_whisper(self, mock_settings, mock_whisper_path, tmp_path):
|
||||||
"""HF_TOKEN 未配置时使用普通 Whisper"""
|
"""HF_TOKEN 未配置时使用普通 Whisper"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_whisper_cpp_path", return_value=mock_whisper_path):
|
with patch("kb_workflow.pipeline.processors.asr.get_whisper_cpp_path", return_value=mock_whisper_path):
|
||||||
with patch("subprocess.run") as mock_run:
|
with patch("subprocess.run") as mock_run:
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.convert_to_wav") as mock_convert:
|
with patch("kb_workflow.pipeline.processors.asr.convert_to_wav") as mock_convert:
|
||||||
mock_convert.return_value = str(tmp_path / "test_audio.wav")
|
mock_convert.return_value = str(tmp_path / "test_audio.wav")
|
||||||
|
|
||||||
with patch.object(processor, "_parse_whisper_output") as mock_parse:
|
with patch.object(processor, "_parse_whisper_output") as mock_parse:
|
||||||
@@ -456,13 +456,13 @@ class TestDiarization:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_diarization_with_hf_token(self, mock_settings_with_hf_token, mock_whisper_path, tmp_path):
|
async def test_diarization_with_hf_token(self, mock_settings_with_hf_token, mock_whisper_path, tmp_path):
|
||||||
"""HF_TOKEN 配置时自动启用说话者分离"""
|
"""HF_TOKEN 配置时自动启用说话者分离"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings_with_hf_token):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings_with_hf_token):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
with patch.object(processor, "_run_whisper_with_diarization") as mock_diarization:
|
with patch.object(processor, "_run_whisper_with_diarization") as mock_diarization:
|
||||||
@@ -493,13 +493,13 @@ class TestDiarization:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_diarization_result_has_speaker(self, mock_settings_with_hf_token, mock_whisper_path, tmp_path):
|
async def test_diarization_result_has_speaker(self, mock_settings_with_hf_token, mock_whisper_path, tmp_path):
|
||||||
"""说话者分离结果包含 speaker 字段"""
|
"""说话者分离结果包含 speaker 字段"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings_with_hf_token):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings_with_hf_token):
|
||||||
processor = ASRProcessor()
|
processor = ASRProcessor()
|
||||||
|
|
||||||
with patch.object(processor, "_run_whisper_basic") as mock_basic:
|
with patch.object(processor, "_run_whisper_basic") as mock_basic:
|
||||||
@@ -513,12 +513,12 @@ class TestDiarization:
|
|||||||
duration_seconds=3.0
|
duration_seconds=3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.run_diarization") as mock_dia:
|
with patch("kb_workflow.pipeline.processors.asr.run_diarization") as mock_dia:
|
||||||
mock_dia.return_value = {
|
mock_dia.return_value = {
|
||||||
"SPEAKER_00": [{"start": 0.0, "end": 3.0}]
|
"SPEAKER_00": [{"start": 0.0, "end": 3.0}]
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.assign_speakers_to_segments") as mock_assign:
|
with patch("kb_workflow.pipeline.processors.asr.assign_speakers_to_segments") as mock_assign:
|
||||||
mock_assign.return_value = [
|
mock_assign.return_value = [
|
||||||
{"start": 0.0, "end": 3.0, "text": "hello", "speaker": "SPEAKER_00"}
|
{"start": 0.0, "end": 3.0, "text": "hello", "speaker": "SPEAKER_00"}
|
||||||
]
|
]
|
||||||
@@ -538,8 +538,8 @@ class TestStateRecording:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_state_recording_success(self, mock_settings, mock_state_manager, tmp_path):
|
async def test_state_recording_success(self, mock_settings, mock_state_manager, tmp_path):
|
||||||
"""ASR 成功时状态记录为 completed"""
|
"""ASR 成功时状态记录为 completed"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput, WorkflowState
|
from kb_workflow.pipeline.schemas import ASRInput, WorkflowState
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
@@ -547,7 +547,7 @@ class TestStateRecording:
|
|||||||
# 创建 mock state
|
# 创建 mock state
|
||||||
mock_state = MagicMock(spec=WorkflowState)
|
mock_state = MagicMock(spec=WorkflowState)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor(state_manager=mock_state_manager)
|
processor = ASRProcessor(state_manager=mock_state_manager)
|
||||||
mock_state_manager.load.return_value = mock_state
|
mock_state_manager.load.return_value = mock_state
|
||||||
|
|
||||||
@@ -575,15 +575,15 @@ class TestStateRecording:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_state_recording_failure(self, mock_settings, mock_state_manager, tmp_path):
|
async def test_state_recording_failure(self, mock_settings, mock_state_manager, tmp_path):
|
||||||
"""ASR 失败时状态记录为 failed"""
|
"""ASR 失败时状态记录为 failed"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput, WorkflowState
|
from kb_workflow.pipeline.schemas import ASRInput, WorkflowState
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
mock_state = MagicMock(spec=WorkflowState)
|
mock_state = MagicMock(spec=WorkflowState)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor(state_manager=mock_state_manager)
|
processor = ASRProcessor(state_manager=mock_state_manager)
|
||||||
mock_state_manager.load.return_value = mock_state
|
mock_state_manager.load.return_value = mock_state
|
||||||
|
|
||||||
@@ -609,13 +609,13 @@ class TestStateRecording:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_state_no_manager(self, mock_settings, tmp_path):
|
async def test_state_no_manager(self, mock_settings, tmp_path):
|
||||||
"""无 state_manager 时不记录状态"""
|
"""无 state_manager 时不记录状态"""
|
||||||
from kb_pipeline.processors.asr import ASRProcessor
|
from kb_workflow.pipeline.processors.asr import ASRProcessor
|
||||||
from kb_pipeline.schemas import ASRInput
|
from kb_workflow.pipeline.schemas import ASRInput
|
||||||
|
|
||||||
video_path = tmp_path / "test.mp4"
|
video_path = tmp_path / "test.mp4"
|
||||||
video_path.write_text("fake video")
|
video_path.write_text("fake video")
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.asr.get_settings", return_value=mock_settings):
|
with patch("kb_workflow.pipeline.processors.asr.get_settings", return_value=mock_settings):
|
||||||
processor = ASRProcessor(state_manager=None)
|
processor = ASRProcessor(state_manager=None)
|
||||||
|
|
||||||
with patch.object(processor, "_try_official_subtitle") as mock_sub:
|
with patch.object(processor, "_try_official_subtitle") as mock_sub:
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ class TestBaseProcessorAbstract:
|
|||||||
|
|
||||||
def test_base_processor_cannot_be_instantiated(self):
|
def test_base_processor_cannot_be_instantiated(self):
|
||||||
"""BaseProcessor 不能直接实例化"""
|
"""BaseProcessor 不能直接实例化"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
BaseProcessor()
|
BaseProcessor()
|
||||||
|
|
||||||
def test_base_processor_is_abstract(self):
|
def test_base_processor_is_abstract(self):
|
||||||
"""BaseProcessor 是抽象类"""
|
"""BaseProcessor 是抽象类"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
|
|
||||||
assert issubclass(BaseProcessor, ABC)
|
assert issubclass(BaseProcessor, ABC)
|
||||||
|
|
||||||
def test_subclass_must_implement_process(self):
|
def test_subclass_must_implement_process(self):
|
||||||
"""子类必须实现 process() 方法"""
|
"""子类必须实现 process() 方法"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
from kb_pipeline.schemas import StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepInput, StepOutput
|
||||||
|
|
||||||
# 缺少 process 实现的子类无法实例化
|
# 缺少 process 实现的子类无法实例化
|
||||||
class IncompleteProcessor(BaseProcessor):
|
class IncompleteProcessor(BaseProcessor):
|
||||||
@@ -42,8 +42,8 @@ class TestBaseProcessorAbstract:
|
|||||||
|
|
||||||
def test_subclass_can_be_instantiated_with_process(self):
|
def test_subclass_can_be_instantiated_with_process(self):
|
||||||
"""实现了 process() 的子类可以实例化"""
|
"""实现了 process() 的子类可以实例化"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
from kb_pipeline.schemas import StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepInput, StepOutput
|
||||||
|
|
||||||
class CompleteProcessor(BaseProcessor):
|
class CompleteProcessor(BaseProcessor):
|
||||||
STEP_NAME = "complete"
|
STEP_NAME = "complete"
|
||||||
@@ -52,7 +52,7 @@ class TestBaseProcessorAbstract:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# 可以实例化
|
# 可以实例化
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = CompleteProcessor()
|
processor = CompleteProcessor()
|
||||||
assert processor is not None
|
assert processor is not None
|
||||||
@@ -63,8 +63,8 @@ class TestBaseProcessorAttributes:
|
|||||||
|
|
||||||
def test_base_processor_has_step_name_class_attribute(self):
|
def test_base_processor_has_step_name_class_attribute(self):
|
||||||
"""BaseProcessor 要求子类定义 STEP_NAME 类属性"""
|
"""BaseProcessor 要求子类定义 STEP_NAME 类属性"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
from kb_pipeline.schemas import StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepInput, StepOutput
|
||||||
|
|
||||||
class TestProcessor(BaseProcessor):
|
class TestProcessor(BaseProcessor):
|
||||||
STEP_NAME = "test_step"
|
STEP_NAME = "test_step"
|
||||||
@@ -72,15 +72,15 @@ class TestBaseProcessorAttributes:
|
|||||||
async def process(self, input_data: StepInput) -> StepOutput:
|
async def process(self, input_data: StepInput) -> StepOutput:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = TestProcessor()
|
processor = TestProcessor()
|
||||||
assert processor.STEP_NAME == "test_step"
|
assert processor.STEP_NAME == "test_step"
|
||||||
|
|
||||||
def test_base_processor_provides_state_manager(self):
|
def test_base_processor_provides_state_manager(self):
|
||||||
"""BaseProcessor 提供 state_manager 属性"""
|
"""BaseProcessor 提供 state_manager 属性"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
from kb_pipeline.schemas import StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepInput, StepOutput
|
||||||
|
|
||||||
class TestProcessor(BaseProcessor):
|
class TestProcessor(BaseProcessor):
|
||||||
STEP_NAME = "test"
|
STEP_NAME = "test"
|
||||||
@@ -90,15 +90,15 @@ class TestBaseProcessorAttributes:
|
|||||||
|
|
||||||
mock_state_manager = MagicMock()
|
mock_state_manager = MagicMock()
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = TestProcessor(state_manager=mock_state_manager)
|
processor = TestProcessor(state_manager=mock_state_manager)
|
||||||
assert processor.state_manager is mock_state_manager
|
assert processor.state_manager is mock_state_manager
|
||||||
|
|
||||||
def test_base_processor_state_manager_optional(self):
|
def test_base_processor_state_manager_optional(self):
|
||||||
"""BaseProcessor state_manager 是可选的"""
|
"""BaseProcessor state_manager 是可选的"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
from kb_pipeline.schemas import StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepInput, StepOutput
|
||||||
|
|
||||||
class TestProcessor(BaseProcessor):
|
class TestProcessor(BaseProcessor):
|
||||||
STEP_NAME = "test"
|
STEP_NAME = "test"
|
||||||
@@ -106,15 +106,15 @@ class TestBaseProcessorAttributes:
|
|||||||
async def process(self, input_data: StepInput) -> StepOutput:
|
async def process(self, input_data: StepInput) -> StepOutput:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
processor = TestProcessor()
|
processor = TestProcessor()
|
||||||
assert processor.state_manager is None
|
assert processor.state_manager is None
|
||||||
|
|
||||||
def test_base_processor_provides_settings(self):
|
def test_base_processor_provides_settings(self):
|
||||||
"""BaseProcessor 提供 settings 属性"""
|
"""BaseProcessor 提供 settings 属性"""
|
||||||
from kb_pipeline.processors.base import BaseProcessor
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
||||||
from kb_pipeline.schemas import StepInput, StepOutput
|
from kb_workflow.pipeline.schemas import StepInput, StepOutput
|
||||||
|
|
||||||
class TestProcessor(BaseProcessor):
|
class TestProcessor(BaseProcessor):
|
||||||
STEP_NAME = "test"
|
STEP_NAME = "test"
|
||||||
@@ -124,7 +124,7 @@ class TestBaseProcessorAttributes:
|
|||||||
|
|
||||||
mock_settings = MagicMock()
|
mock_settings = MagicMock()
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_get_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_get_settings:
|
||||||
mock_get_settings.return_value = mock_settings
|
mock_get_settings.return_value = mock_settings
|
||||||
processor = TestProcessor()
|
processor = TestProcessor()
|
||||||
assert processor.settings is mock_settings
|
assert processor.settings is mock_settings
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from datetime import datetime
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from kb_pipeline.schemas import DownloadInput, DownloadOutput
|
from kb_workflow.pipeline.schemas import DownloadInput, DownloadOutput
|
||||||
|
|
||||||
|
|
||||||
class TestDownloadInputSchema:
|
class TestDownloadInputSchema:
|
||||||
@@ -180,8 +180,8 @@ class TestDownloadProcessorMetadata:
|
|||||||
self, mock_bilibili_adapter, mock_state_manager, tmp_path
|
self, mock_bilibili_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 1: DownloadProcessor fetches Bilibili video metadata correctly"""
|
"""Test 1: DownloadProcessor fetches Bilibili video metadata correctly"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput
|
from kb_workflow.pipeline.schemas import DownloadInput
|
||||||
|
|
||||||
input_data = DownloadInput(
|
input_data = DownloadInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
@@ -190,7 +190,7 @@ class TestDownloadProcessorMetadata:
|
|||||||
content_type="video"
|
content_type="video"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
|
||||||
@@ -212,8 +212,8 @@ class TestDownloadProcessorMetadata:
|
|||||||
self, mock_zhihu_adapter, mock_state_manager, tmp_path
|
self, mock_zhihu_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 2: DownloadProcessor fetches Zhihu answer metadata correctly"""
|
"""Test 2: DownloadProcessor fetches Zhihu answer metadata correctly"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput
|
from kb_workflow.pipeline.schemas import DownloadInput
|
||||||
|
|
||||||
input_data = DownloadInput(
|
input_data = DownloadInput(
|
||||||
content_id="456",
|
content_id="456",
|
||||||
@@ -222,7 +222,7 @@ class TestDownloadProcessorMetadata:
|
|||||||
content_type="answer"
|
content_type="answer"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_zhihu_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_zhihu_adapter
|
||||||
@@ -243,8 +243,8 @@ class TestDownloadProcessorMetadata:
|
|||||||
self, mock_bilibili_adapter, mock_state_manager, tmp_path
|
self, mock_bilibili_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 3: DownloadProcessor records state on success"""
|
"""Test 3: DownloadProcessor records state on success"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput, WorkflowState
|
from kb_workflow.pipeline.schemas import DownloadInput, WorkflowState
|
||||||
|
|
||||||
# Create a mock state that will be updated
|
# Create a mock state that will be updated
|
||||||
mock_state = MagicMock(spec=WorkflowState)
|
mock_state = MagicMock(spec=WorkflowState)
|
||||||
@@ -258,7 +258,7 @@ class TestDownloadProcessorMetadata:
|
|||||||
content_type="video"
|
content_type="video"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
|
||||||
@@ -279,9 +279,9 @@ class TestDownloadProcessorMetadata:
|
|||||||
self, mock_bilibili_adapter, mock_state_manager, tmp_path
|
self, mock_bilibili_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 4: DownloadProcessor records state on failure"""
|
"""Test 4: DownloadProcessor records state on failure"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput, WorkflowState
|
from kb_workflow.pipeline.schemas import DownloadInput, WorkflowState
|
||||||
from kb_pipeline.errors import NetworkError
|
from kb_workflow.pipeline.errors import NetworkError
|
||||||
|
|
||||||
# Make adapter raise an error
|
# Make adapter raise an error
|
||||||
mock_bilibili_adapter.fetch_metadata = AsyncMock(
|
mock_bilibili_adapter.fetch_metadata = AsyncMock(
|
||||||
@@ -305,7 +305,7 @@ class TestDownloadProcessorMetadata:
|
|||||||
content_type="video"
|
content_type="video"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
|
||||||
@@ -369,8 +369,8 @@ class TestDownloadProcessorContent:
|
|||||||
self, mock_bilibili_article_adapter, mock_state_manager, tmp_path
|
self, mock_bilibili_article_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 1: DownloadProcessor downloads Bilibili article content"""
|
"""Test 1: DownloadProcessor downloads Bilibili article content"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput
|
from kb_workflow.pipeline.schemas import DownloadInput
|
||||||
|
|
||||||
input_data = DownloadInput(
|
input_data = DownloadInput(
|
||||||
content_id="CV123",
|
content_id="CV123",
|
||||||
@@ -379,7 +379,7 @@ class TestDownloadProcessorContent:
|
|||||||
content_type="article"
|
content_type="article"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_article_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_article_adapter
|
||||||
@@ -403,8 +403,8 @@ class TestDownloadProcessorContent:
|
|||||||
self, mock_bilibili_video_adapter, mock_state_manager, tmp_path
|
self, mock_bilibili_video_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 2: DownloadProcessor skips video download (handled by ASR)"""
|
"""Test 2: DownloadProcessor skips video download (handled by ASR)"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput
|
from kb_workflow.pipeline.schemas import DownloadInput
|
||||||
|
|
||||||
input_data = DownloadInput(
|
input_data = DownloadInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
@@ -413,7 +413,7 @@ class TestDownloadProcessorContent:
|
|||||||
content_type="video"
|
content_type="video"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_video_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_video_adapter
|
||||||
@@ -433,8 +433,8 @@ class TestDownloadProcessorContent:
|
|||||||
self, mock_bilibili_article_adapter, mock_state_manager, tmp_path
|
self, mock_bilibili_article_adapter, mock_state_manager, tmp_path
|
||||||
):
|
):
|
||||||
"""Test 3: DownloadProcessor creates correct output file paths"""
|
"""Test 3: DownloadProcessor creates correct output file paths"""
|
||||||
from kb_pipeline.processors.download import DownloadProcessor
|
from kb_workflow.pipeline.processors.download import DownloadProcessor
|
||||||
from kb_pipeline.schemas import DownloadInput
|
from kb_workflow.pipeline.schemas import DownloadInput
|
||||||
|
|
||||||
input_data = DownloadInput(
|
input_data = DownloadInput(
|
||||||
content_id="CV123",
|
content_id="CV123",
|
||||||
@@ -443,7 +443,7 @@ class TestDownloadProcessorContent:
|
|||||||
content_type="article"
|
content_type="article"
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_article_adapter
|
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_article_adapter
|
||||||
@@ -466,16 +466,16 @@ class TestDownloadProcessorModuleExport:
|
|||||||
"""Tests for DownloadProcessor module export."""
|
"""Tests for DownloadProcessor module export."""
|
||||||
|
|
||||||
def test_download_processor_importable_from_processors(self):
|
def test_download_processor_importable_from_processors(self):
|
||||||
"""Test 4: DownloadProcessor can be imported from kb_pipeline.processors"""
|
"""Test 4: DownloadProcessor can be imported from kb_workflow.pipeline.processors"""
|
||||||
from kb_pipeline.processors import DownloadProcessor
|
from kb_workflow.pipeline.processors import DownloadProcessor
|
||||||
|
|
||||||
assert DownloadProcessor is not None
|
assert DownloadProcessor is not None
|
||||||
assert hasattr(DownloadProcessor, 'STEP_NAME')
|
assert hasattr(DownloadProcessor, 'STEP_NAME')
|
||||||
assert DownloadProcessor.STEP_NAME == "download"
|
assert DownloadProcessor.STEP_NAME == "download"
|
||||||
|
|
||||||
def test_all_processors_exported(self):
|
def test_all_processors_exported(self):
|
||||||
"""All processors should be exported from kb_pipeline.processors"""
|
"""All processors should be exported from kb_workflow.pipeline.processors"""
|
||||||
from kb_pipeline.processors import BaseProcessor, ParseProcessor, DownloadProcessor, ASRProcessor
|
from kb_workflow.pipeline.processors import BaseProcessor, ParseProcessor, DownloadProcessor, ASRProcessor
|
||||||
|
|
||||||
assert BaseProcessor is not None
|
assert BaseProcessor is not None
|
||||||
assert ParseProcessor is not None
|
assert ParseProcessor is not None
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Tests that ParseProcessor:
|
|||||||
- Parses Zhihu answer URLs correctly
|
- Parses Zhihu answer URLs correctly
|
||||||
- Returns ParseOutput with success=False for unknown URLs
|
- Returns ParseOutput with success=False for unknown URLs
|
||||||
- Records state when state_manager is provided
|
- Records state when state_manager is provided
|
||||||
- Can be imported from kb_pipeline.processors
|
- Can be imported from kb_workflow.pipeline.processors
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -18,10 +18,10 @@ class TestParseProcessorBilibili:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_parse_bilibili_video_url(self):
|
async def test_parse_bilibili_video_url(self):
|
||||||
"""ParseProcessor 正确解析 Bilibili 视频 URL"""
|
"""ParseProcessor 正确解析 Bilibili 视频 URL"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor()
|
processor = ParseProcessor()
|
||||||
@@ -38,10 +38,10 @@ class TestParseProcessorBilibili:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_parse_bilibili_article_url(self):
|
async def test_parse_bilibili_article_url(self):
|
||||||
"""ParseProcessor 正确解析 Bilibili 专栏 URL"""
|
"""ParseProcessor 正确解析 Bilibili 专栏 URL"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor()
|
processor = ParseProcessor()
|
||||||
@@ -61,10 +61,10 @@ class TestParseProcessorZhihu:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_parse_zhihu_answer_url(self):
|
async def test_parse_zhihu_answer_url(self):
|
||||||
"""ParseProcessor 正确解析 Zhihu 回答 URL"""
|
"""ParseProcessor 正确解析 Zhihu 回答 URL"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor()
|
processor = ParseProcessor()
|
||||||
@@ -80,10 +80,10 @@ class TestParseProcessorZhihu:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_parse_zhihu_zhuanlan_url(self):
|
async def test_parse_zhihu_zhuanlan_url(self):
|
||||||
"""ParseProcessor 正确解析 Zhihu 专栏 URL"""
|
"""ParseProcessor 正确解析 Zhihu 专栏 URL"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor()
|
processor = ParseProcessor()
|
||||||
@@ -103,10 +103,10 @@ class TestParseProcessorUnknown:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_parse_unknown_url_returns_failure(self):
|
async def test_parse_unknown_url_returns_failure(self):
|
||||||
"""ParseProcessor 对未知 URL 返回 success=False"""
|
"""ParseProcessor 对未知 URL 返回 success=False"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor()
|
processor = ParseProcessor()
|
||||||
@@ -124,14 +124,14 @@ class TestParseProcessorStateRecording:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_state_recording_on_success(self):
|
async def test_state_recording_on_success(self):
|
||||||
"""ParseProcessor 成功时记录状态"""
|
"""ParseProcessor 成功时记录状态"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput, WorkflowState
|
from kb_workflow.pipeline.schemas import ParseInput, WorkflowState
|
||||||
|
|
||||||
mock_state_manager = MagicMock()
|
mock_state_manager = MagicMock()
|
||||||
mock_state = MagicMock(spec=WorkflowState)
|
mock_state = MagicMock(spec=WorkflowState)
|
||||||
mock_state_manager.load.return_value = mock_state
|
mock_state_manager.load.return_value = mock_state
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor(state_manager=mock_state_manager)
|
processor = ParseProcessor(state_manager=mock_state_manager)
|
||||||
@@ -147,10 +147,10 @@ class TestParseProcessorStateRecording:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_no_state_recording_without_manager(self):
|
async def test_no_state_recording_without_manager(self):
|
||||||
"""无 state_manager 时不记录状态"""
|
"""无 state_manager 时不记录状态"""
|
||||||
from kb_pipeline.processors.parse import ParseProcessor
|
from kb_workflow.pipeline.processors.parse import ParseProcessor
|
||||||
from kb_pipeline.schemas import ParseInput
|
from kb_workflow.pipeline.schemas import ParseInput
|
||||||
|
|
||||||
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
|
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
|
||||||
mock_settings.return_value = MagicMock()
|
mock_settings.return_value = MagicMock()
|
||||||
|
|
||||||
processor = ParseProcessor(state_manager=None)
|
processor = ParseProcessor(state_manager=None)
|
||||||
@@ -167,18 +167,18 @@ class TestParseProcessorModuleExport:
|
|||||||
|
|
||||||
def test_parse_processor_importable_from_processors(self):
|
def test_parse_processor_importable_from_processors(self):
|
||||||
"""ParseProcessor 可以从 kb_pipeline.processors 导入"""
|
"""ParseProcessor 可以从 kb_pipeline.processors 导入"""
|
||||||
from kb_pipeline.processors import ParseProcessor
|
from kb_workflow.pipeline.processors import ParseProcessor
|
||||||
|
|
||||||
assert ParseProcessor is not None
|
assert ParseProcessor is not None
|
||||||
|
|
||||||
def test_base_processor_importable_from_processors(self):
|
def test_base_processor_importable_from_processors(self):
|
||||||
"""BaseProcessor 可以从 kb_pipeline.processors 导入"""
|
"""BaseProcessor 可以从 kb_pipeline.processors 导入"""
|
||||||
from kb_pipeline.processors import BaseProcessor
|
from kb_workflow.pipeline.processors import BaseProcessor
|
||||||
|
|
||||||
assert BaseProcessor is not None
|
assert BaseProcessor is not None
|
||||||
|
|
||||||
def test_asr_processor_still_importable(self):
|
def test_asr_processor_still_importable(self):
|
||||||
"""ASRProcessor 仍然可以从 kb_pipeline.processors 导入"""
|
"""ASRProcessor 仍然可以从 kb_pipeline.processors 导入"""
|
||||||
from kb_pipeline.processors import ASRProcessor
|
from kb_workflow.pipeline.processors import ASRProcessor
|
||||||
|
|
||||||
assert ASRProcessor is not None
|
assert ASRProcessor is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user