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:
2026-03-17 00:17:10 +08:00
parent fdca690bd6
commit c9ca1b538d
20 changed files with 307 additions and 307 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from unittest.mock import patch, MagicMock
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:
+1 -1
View File
@@ -4,7 +4,7 @@ Tests for KBError, NetworkError, ValidationError, SystemError, AuthError and Err
"""
import pytest
from kb_pipeline.errors import (
from kb_workflow.pipeline.errors import (
KBError,
NetworkError,
ValidationError,
+8 -8
View File
@@ -15,7 +15,7 @@ class TestIdempotencyChecker:
def test_check_exists_returns_true_when_files_exist(self, tmp_path):
"""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
output_dir = tmp_path / "bilibili"
@@ -41,7 +41,7 @@ class TestIdempotencyChecker:
def test_check_exists_returns_false_when_files_not_exist(self, tmp_path):
"""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.mkdir()
@@ -57,7 +57,7 @@ class TestIdempotencyChecker:
def test_check_exists_returns_partial_files_when_some_exist(self, tmp_path):
"""Test 3: 部分文件存在时返回正确的存在文件列表"""
from kb_pipeline.idempotency import IdempotencyChecker
from kb_workflow.pipeline.idempotency import IdempotencyChecker
output_dir = tmp_path / "zhihu"
output_dir.mkdir()
@@ -80,7 +80,7 @@ class TestIdempotencyChecker:
def test_check_exists_creates_directory_if_not_exists(self, tmp_path):
"""如果输出目录不存在,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
content_id = "BV1test123"
@@ -98,7 +98,7 @@ class TestIdempotencyCheckerFileNaming:
def test_bilibili_file_naming(self, tmp_path):
"""测试B站文件的命名格式"""
from kb_pipeline.idempotency import IdempotencyChecker
from kb_workflow.pipeline.idempotency import IdempotencyChecker
output_dir = tmp_path / "bilibili"
output_dir.mkdir()
@@ -121,7 +121,7 @@ class TestIdempotencyCheckerFileNaming:
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.mkdir()
@@ -148,7 +148,7 @@ class TestIdempotencyCheckerEdgeCases:
def test_check_exists_with_empty_content_id(self, tmp_path):
"""空content_id应该返回False"""
from kb_pipeline.idempotency import IdempotencyChecker
from kb_workflow.pipeline.idempotency import IdempotencyChecker
output_dir = tmp_path / "bilibili"
output_dir.mkdir()
@@ -161,7 +161,7 @@ class TestIdempotencyCheckerEdgeCases:
def test_check_exists_with_special_characters_in_id(self, tmp_path):
"""ID中包含特殊字符时的处理"""
from kb_pipeline.idempotency import IdempotencyChecker
from kb_workflow.pipeline.idempotency import IdempotencyChecker
output_dir = tmp_path / "bilibili"
output_dir.mkdir()
+17 -17
View File
@@ -18,7 +18,7 @@ class TestLinkParserBilibiliVideo:
def test_parse_bilibili_video_url_returns_correct_parsed_link(self):
"""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()
result = parser.parse("https://www.bilibili.com/video/BV1GJ411x7Qw")
@@ -31,7 +31,7 @@ class TestLinkParserBilibiliVideo:
def test_parse_bilibili_video_url_without_www(self):
"""B站视频URL不带www也能正确解析"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
result = parser.parse("https://bilibili.com/video/BV1GJ411x7Qw")
@@ -43,7 +43,7 @@ class TestLinkParserBilibiliVideo:
def test_parse_bilibili_video_url_with_http(self):
"""B站视频URL使用http也能正确解析"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
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):
"""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()
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):
"""B站专栏URL使用article前缀也能正确解析"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
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):
"""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()
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):
"""知乎问答URL不带www也能正确解析"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
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):
"""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()
result = parser.parse("https://zhuanlan.zhihu.com/p/12345678")
@@ -127,10 +127,10 @@ class TestLinkParserZhihuZhuanlan:
class TestLinkParserShortLink:
"""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):
"""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_response = MagicMock()
@@ -147,10 +147,10 @@ class TestLinkParserShortLink:
assert result.content_id == "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):
"""短链解析应该使用 allow_redirects=True"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
mock_response = MagicMock()
mock_response.status_code = 200
@@ -170,7 +170,7 @@ class TestLinkParserUnknownURL:
def test_parse_unknown_url_returns_none(self):
"""Test 6: 无法识别的URL返回None(不抛异常)"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
result = parser.parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
@@ -179,7 +179,7 @@ class TestLinkParserUnknownURL:
def test_parse_invalid_url_returns_none(self):
"""无效URL返回None"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
result = parser.parse("not a valid url")
@@ -188,7 +188,7 @@ class TestLinkParserUnknownURL:
def test_parse_empty_string_returns_none(self):
"""空字符串返回None"""
from kb_pipeline.link_parser import LinkParser
from kb_workflow.pipeline.link_parser import LinkParser
parser = LinkParser()
result = parser.parse("")
@@ -201,7 +201,7 @@ class TestParsedLink:
def test_parsed_link_has_all_required_fields(self):
"""ParsedLink 应包含所有必需字段"""
from kb_pipeline.link_parser import ParsedLink
from kb_workflow.pipeline.link_parser import ParsedLink
link = ParsedLink(
platform="bilibili",
@@ -219,7 +219,7 @@ class TestParsedLink:
def test_parsed_link_resolved_url_defaults_to_original(self):
"""ParsedLink 的 resolved_url 默认等于 original_url"""
from kb_pipeline.link_parser import ParsedLink
from kb_workflow.pipeline.link_parser import ParsedLink
link = ParsedLink(
platform="bilibili",
+40 -40
View File
@@ -20,7 +20,7 @@ class TestPipelineInput:
def test_schema_pipeline_input_validates_url_field(self):
"""PipelineInput must include url field (required)"""
from kb_pipeline.schemas import PipelineInput
from kb_workflow.pipeline.schemas import PipelineInput
# Missing url
with pytest.raises(ValidationError) as exc_info:
@@ -37,7 +37,7 @@ class TestPipelineInput:
def test_schema_pipeline_input_has_optional_output_dir(self):
"""PipelineInput has optional output_dir field"""
from kb_pipeline.schemas import PipelineInput
from kb_workflow.pipeline.schemas import PipelineInput
# Without output_dir
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):
"""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")
assert input_data.skip_completed is True
@@ -70,7 +70,7 @@ class TestPipelineOutput:
def test_schema_pipeline_output_contains_results_dict(self):
"""PipelineOutput contains results dict"""
from kb_pipeline.schemas import PipelineOutput
from kb_workflow.pipeline.schemas import PipelineOutput
output = PipelineOutput(success=True)
assert hasattr(output, "results")
@@ -79,7 +79,7 @@ class TestPipelineOutput:
def test_schema_pipeline_output_has_success_field(self):
"""PipelineOutput requires success field"""
from kb_pipeline.schemas import PipelineOutput
from kb_workflow.pipeline.schemas import PipelineOutput
with pytest.raises(ValidationError) as exc_info:
PipelineOutput()
@@ -87,7 +87,7 @@ class TestPipelineOutput:
def test_schema_pipeline_output_has_optional_error_message(self):
"""PipelineOutput has optional error_message field"""
from kb_pipeline.schemas import PipelineOutput
from kb_workflow.pipeline.schemas import PipelineOutput
output = PipelineOutput(success=True)
assert output.error_message is None
@@ -100,7 +100,7 @@ class TestPipelineOutput:
def test_schema_pipeline_output_has_content_dir(self):
"""PipelineOutput has optional content_dir field"""
from kb_pipeline.schemas import PipelineOutput
from kb_workflow.pipeline.schemas import PipelineOutput
output = PipelineOutput(success=True)
assert output.content_dir is None
@@ -117,7 +117,7 @@ class TestPipelineConfig:
def test_schema_pipeline_config_has_default_steps_list(self):
"""PipelineConfig has default steps list"""
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.schemas import PipelineConfig
config = PipelineConfig()
assert hasattr(config, "steps")
@@ -126,14 +126,14 @@ class TestPipelineConfig:
def test_schema_pipeline_config_allows_custom_steps(self):
"""PipelineConfig allows custom steps list"""
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.schemas import PipelineConfig
custom_config = PipelineConfig(steps=["parse", "download"])
assert custom_config.steps == ["parse", "download"]
def test_schema_pipeline_config_allows_custom_output_dir(self):
"""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"))
assert config.output_dir == Path("/custom/output")
@@ -144,14 +144,14 @@ class TestPipelineConfig:
def test_schema_pipeline_config_has_skip_completed_default(self):
"""PipelineConfig skip_completed defaults to True"""
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.schemas import PipelineConfig
config = PipelineConfig()
assert config.skip_completed is True
def test_schema_pipeline_config_has_max_retries_default(self):
"""PipelineConfig max_retries defaults to 3"""
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.schemas import PipelineConfig
config = PipelineConfig()
assert config.max_retries == 3
@@ -171,8 +171,8 @@ class TestPipelineInit:
def test_init_pipeline_with_processors(self):
"""Pipeline initializes with all processors"""
from kb_pipeline.pipeline import Pipeline
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.pipeline import Pipeline
from kb_workflow.pipeline.schemas import PipelineConfig
mock_processor1 = MagicMock()
mock_processor1.STEP_NAME = "parse"
@@ -180,7 +180,7 @@ class TestPipelineInit:
mock_processor2 = MagicMock()
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()
pipeline = Pipeline(processors=[mock_processor1, mock_processor2])
@@ -192,15 +192,15 @@ class TestPipelineInit:
def test_init_pipeline_validates_processor_step_name(self):
"""Pipeline validates processor STEP_NAME matches config.steps"""
from kb_pipeline.pipeline import Pipeline
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.pipeline import Pipeline
from kb_workflow.pipeline.schemas import PipelineConfig
mock_processor = MagicMock()
mock_processor.STEP_NAME = "unknown_step"
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()
with pytest.raises(ValueError) as exc_info:
@@ -210,13 +210,13 @@ class TestPipelineInit:
def test_init_pipeline_handles_missing_processor_gracefully(self):
"""Pipeline handles missing processor gracefully"""
from kb_pipeline.pipeline import Pipeline
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.pipeline import Pipeline
from kb_workflow.pipeline.schemas import PipelineConfig
# Empty processors list should work
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()
pipeline = Pipeline(processors=[], config=config)
@@ -228,14 +228,14 @@ class TestPipelineParseUrl:
def test_parse_url_creates_correct_content_directory(self):
"""Pipeline._get_content_dir creates correct content directory"""
from kb_pipeline.pipeline import Pipeline
from kb_pipeline.link_parser import ParsedLink
from kb_workflow.pipeline.pipeline import Pipeline
from kb_workflow.pipeline.link_parser import ParsedLink
import tempfile
mock_processor = MagicMock()
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()
with tempfile.TemporaryDirectory() as tmpdir:
@@ -258,12 +258,12 @@ class TestPipelineParseUrl:
def test_parse_url_returns_none_for_empty_url(self):
"""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.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()
pipeline = Pipeline(processors=[mock_processor])
@@ -273,12 +273,12 @@ class TestPipelineParseUrl:
def test_parse_url_returns_none_for_invalid_url(self):
"""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.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()
pipeline = Pipeline(processors=[mock_processor])
@@ -288,12 +288,12 @@ class TestPipelineParseUrl:
def test_parse_url_parses_bilibili_url(self):
"""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.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()
pipeline = Pipeline(processors=[mock_processor])
@@ -373,8 +373,8 @@ class TestPipelineArchiveStep:
@pytest.mark.asyncio
async def test_pipeline_includes_archive_step(self, mock_processors_with_archive, tmp_path):
"""Test that pipeline includes archive as final step"""
from kb_pipeline.pipeline import Pipeline
from kb_pipeline.schemas import PipelineConfig
from kb_workflow.pipeline.pipeline import Pipeline
from kb_workflow.pipeline.schemas import PipelineConfig
config = PipelineConfig(
steps=["parse", "download", "asr", "analyze", "archive"],
@@ -382,7 +382,7 @@ class TestPipelineArchiveStep:
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))
pipeline = Pipeline(
@@ -395,7 +395,7 @@ class TestPipelineArchiveStep:
with patch.object(pipeline, '_parse_url') as mock_parse, \
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(
platform="bilibili",
content_type="video",
@@ -419,8 +419,8 @@ class TestArchivePlatformDirectory:
@pytest.mark.asyncio
async def test_archive_uses_platform_directory(self, tmp_path):
"""Test that archive files go to {archive_path}/{platform}/ directory"""
from kb_pipeline.processors.archive import ArchiveProcessor
from kb_pipeline.schemas import ArchiveInput
from kb_workflow.pipeline.processors.archive import ArchiveProcessor
from kb_workflow.pipeline.schemas import ArchiveInput
# Setup
archive_dir = tmp_path / "archive"
@@ -430,7 +430,7 @@ class TestArchivePlatformDirectory:
analysis_file.write_text("# Test")
# 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.archive_path = str(archive_dir)
mock_get_settings.return_value = mock_settings
@@ -461,8 +461,8 @@ class TestArchiveNoDateSubdirectories:
@pytest.mark.asyncio
async def test_archive_no_date_subdirectories(self, tmp_path):
"""Test that archive does not create date-based subdirectories"""
from kb_pipeline.processors.archive import ArchiveProcessor
from kb_pipeline.schemas import ArchiveInput
from kb_workflow.pipeline.processors.archive import ArchiveProcessor
from kb_workflow.pipeline.schemas import ArchiveInput
archive_dir = tmp_path / "archive"
content_dir = tmp_path / "intermediate" / "bilibili_BV1test"
@@ -470,7 +470,7 @@ class TestArchiveNoDateSubdirectories:
analysis_file = content_dir / "analysis.md"
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.archive_path = str(archive_dir)
mock_get_settings.return_value = mock_settings
+2 -2
View File
@@ -7,8 +7,8 @@ import pytest
from unittest.mock import MagicMock, patch
import time
from kb_pipeline.errors import NetworkError, ValidationError, ErrorType
from kb_pipeline.retry import retry_on_network_error
from kb_workflow.pipeline.errors import NetworkError, ValidationError, ErrorType
from kb_workflow.pipeline.retry import retry_on_network_error
class TestRetryOnNetworkError:
+43 -43
View File
@@ -13,7 +13,7 @@ class TestStepInput:
def test_step_input_requires_content_id(self):
"""StepInput 必须包含 content_id 字段"""
from kb_pipeline.schemas import StepInput
from kb_workflow.pipeline.schemas import StepInput
with pytest.raises(ValidationError) as exc_info:
StepInput(platform="bilibili")
@@ -22,7 +22,7 @@ class TestStepInput:
def test_step_input_requires_platform(self):
"""StepInput 必须包含 platform 字段"""
from kb_pipeline.schemas import StepInput
from kb_workflow.pipeline.schemas import StepInput
with pytest.raises(ValidationError) as exc_info:
StepInput(content_id="BV1test123")
@@ -31,7 +31,7 @@ class TestStepInput:
def test_step_input_validates_platform_literal(self):
"""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")
@@ -46,7 +46,7 @@ class TestStepInput:
def test_step_input_content_id_non_empty(self):
"""StepInput content_id 不能为空字符串"""
from kb_pipeline.schemas import StepInput
from kb_workflow.pipeline.schemas import StepInput
with pytest.raises(ValidationError):
StepInput(content_id="", platform="bilibili")
@@ -57,7 +57,7 @@ class TestStepOutput:
def test_step_output_requires_success(self):
"""StepOutput 必须包含 success 字段"""
from kb_pipeline.schemas import StepOutput
from kb_workflow.pipeline.schemas import StepOutput
with pytest.raises(ValidationError) as exc_info:
StepOutput()
@@ -66,7 +66,7 @@ class TestStepOutput:
def test_step_output_default_completed_at(self):
"""StepOutput completed_at 默认为当前时间"""
from kb_pipeline.schemas import StepOutput
from kb_workflow.pipeline.schemas import StepOutput
before = datetime.now()
output = StepOutput(success=True)
@@ -76,7 +76,7 @@ class TestStepOutput:
def test_step_output_optional_error_message(self):
"""StepOutput error_message 是可选的"""
from kb_pipeline.schemas import StepOutput
from kb_workflow.pipeline.schemas import StepOutput
output = StepOutput(success=True)
assert output.error_message is None
@@ -90,31 +90,31 @@ class TestStepStatus:
def test_step_status_has_pending(self):
"""StepStatus 包含 PENDING 状态"""
from kb_pipeline.schemas import StepStatus
from kb_workflow.pipeline.schemas import StepStatus
assert StepStatus.PENDING.value == "pending"
def test_step_status_has_running(self):
"""StepStatus 包含 RUNNING 状态"""
from kb_pipeline.schemas import StepStatus
from kb_workflow.pipeline.schemas import StepStatus
assert StepStatus.RUNNING.value == "running"
def test_step_status_has_completed(self):
"""StepStatus 包含 COMPLETED 状态"""
from kb_pipeline.schemas import StepStatus
from kb_workflow.pipeline.schemas import StepStatus
assert StepStatus.COMPLETED.value == "completed"
def test_step_status_has_failed(self):
"""StepStatus 包含 FAILED 状态"""
from kb_pipeline.schemas import StepStatus
from kb_workflow.pipeline.schemas import StepStatus
assert StepStatus.FAILED.value == "failed"
def test_step_status_has_skipped(self):
"""StepStatus 包含 SKIPPED 状态"""
from kb_pipeline.schemas import StepStatus
from kb_workflow.pipeline.schemas import StepStatus
assert StepStatus.SKIPPED.value == "skipped"
@@ -124,21 +124,21 @@ class TestStepRecord:
def test_step_record_requires_step_name(self):
"""StepRecord 必须包含 step_name"""
from kb_pipeline.schemas import StepRecord, StepStatus
from kb_workflow.pipeline.schemas import StepRecord, StepStatus
with pytest.raises(ValidationError):
StepRecord(status=StepStatus.COMPLETED)
def test_step_record_requires_status(self):
"""StepRecord 必须包含 status"""
from kb_pipeline.schemas import StepRecord
from kb_workflow.pipeline.schemas import StepRecord
with pytest.raises(ValidationError):
StepRecord(step_name="fetch_metadata")
def test_step_record_optional_fields(self):
"""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)
assert record.started_at is None
@@ -152,28 +152,28 @@ class TestWorkflowState:
def test_workflow_state_requires_content_id(self):
"""WorkflowState 必须包含 content_id"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
with pytest.raises(ValidationError):
WorkflowState(platform="bilibili", source_url="https://test.com")
def test_workflow_state_requires_platform(self):
"""WorkflowState 必须包含 platform"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
with pytest.raises(ValidationError):
WorkflowState(content_id="BV1test", source_url="https://test.com")
def test_workflow_state_requires_source_url(self):
"""WorkflowState 必须包含 source_url"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
with pytest.raises(ValidationError):
WorkflowState(content_id="BV1test", platform="bilibili")
def test_workflow_state_has_steps_dict(self):
"""WorkflowState 包含 steps 字典"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
state = WorkflowState(
content_id="BV1test",
@@ -185,7 +185,7 @@ class TestWorkflowState:
def test_workflow_state_default_schema_version(self):
"""WorkflowState schema_version 默认为 1.0"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
state = WorkflowState(
content_id="BV1test",
@@ -196,7 +196,7 @@ class TestWorkflowState:
def test_workflow_state_is_step_completed_method(self):
"""WorkflowState 有 is_step_completed 方法"""
from kb_pipeline.schemas import WorkflowState, StepStatus, StepRecord
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus, StepRecord
state = WorkflowState(
content_id="BV1test",
@@ -220,7 +220,7 @@ class TestWorkflowStateMethods:
def test_mark_step_started(self):
"""mark_step_started 正确设置步骤状态"""
from kb_pipeline.schemas import WorkflowState, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
state = WorkflowState(
content_id="BV1test",
@@ -236,7 +236,7 @@ class TestWorkflowStateMethods:
def test_mark_step_completed_existing_step(self):
"""mark_step_completed 更新已存在的步骤"""
from kb_pipeline.schemas import WorkflowState, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
state = WorkflowState(
content_id="BV1test",
@@ -253,7 +253,7 @@ class TestWorkflowStateMethods:
def test_mark_step_completed_new_step(self):
"""mark_step_completed 创建新步骤记录"""
from kb_pipeline.schemas import WorkflowState, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
state = WorkflowState(
content_id="BV1test",
@@ -269,7 +269,7 @@ class TestWorkflowStateMethods:
def test_mark_step_failed_existing_step(self):
"""mark_step_failed 更新已存在的步骤为失败"""
from kb_pipeline.schemas import WorkflowState, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
state = WorkflowState(
content_id="BV1test",
@@ -286,7 +286,7 @@ class TestWorkflowStateMethods:
def test_mark_step_failed_new_step(self):
"""mark_step_failed 创建新的失败步骤记录"""
from kb_pipeline.schemas import WorkflowState, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepStatus
state = WorkflowState(
content_id="BV1test",
@@ -306,7 +306,7 @@ class TestStepResult:
def test_step_result_creation(self):
"""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")
result = StepResult(step_name="fetch_metadata", input_data=input_data)
@@ -317,7 +317,7 @@ class TestStepResult:
def test_step_result_with_output(self):
"""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")
output_data = StepOutput(success=True)
@@ -337,7 +337,7 @@ class TestStepDefinition:
def test_step_definition_creation(self):
"""StepDefinition 正确创建"""
from kb_pipeline.schemas import StepDefinition, StepInput, StepOutput
from kb_workflow.pipeline.schemas import StepDefinition, StepInput, StepOutput
definition = StepDefinition(
step_name="fetch_metadata",
@@ -352,7 +352,7 @@ class TestStepDefinition:
def test_step_definition_with_dependencies(self):
"""StepDefinition 包含依赖"""
from kb_pipeline.schemas import StepDefinition, StepInput, StepOutput
from kb_workflow.pipeline.schemas import StepDefinition, StepInput, StepOutput
definition = StepDefinition(
step_name="transcribe_audio",
@@ -371,14 +371,14 @@ class TestSchemaValidation:
def test_validation_error_on_invalid_input(self):
"""无效输入抛出 ValidationError"""
from kb_pipeline.schemas import StepInput
from kb_workflow.pipeline.schemas import StepInput
with pytest.raises(ValidationError):
StepInput(content_id=123, platform="bilibili") # type: ignore
def test_model_validate_from_dict(self):
"""model_validate 从字典正确创建实例"""
from kb_pipeline.schemas import StepInput
from kb_workflow.pipeline.schemas import StepInput
data = {"content_id": "BV1test", "platform": "bilibili"}
step_input = StepInput.model_validate(data)
@@ -391,7 +391,7 @@ class TestJSONSerialization:
def test_step_input_model_dump_json(self):
"""StepInput 支持 model_dump_json"""
from kb_pipeline.schemas import StepInput
from kb_workflow.pipeline.schemas import StepInput
import json
step_input = StepInput(content_id="BV1test", platform="bilibili")
@@ -403,7 +403,7 @@ class TestJSONSerialization:
def test_step_output_model_dump_json(self):
"""StepOutput 支持 model_dump_json"""
from kb_pipeline.schemas import StepOutput
from kb_workflow.pipeline.schemas import StepOutput
import json
output = StepOutput(success=True, error_message=None)
@@ -415,7 +415,7 @@ class TestJSONSerialization:
def test_workflow_state_model_dump_json(self):
"""WorkflowState 支持 model_dump_json"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
import json
state = WorkflowState(
@@ -433,7 +433,7 @@ class TestJSONSerialization:
def test_workflow_state_model_validate_from_json(self):
"""WorkflowState 可从 JSON 恢复"""
from kb_pipeline.schemas import WorkflowState
from kb_workflow.pipeline.schemas import WorkflowState
import json
original = WorkflowState(
@@ -450,7 +450,7 @@ class TestJSONSerialization:
def test_workflow_state_with_steps_serialization(self):
"""WorkflowState steps 字典正确序列化/反序列化"""
from kb_pipeline.schemas import WorkflowState, StepRecord, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepRecord, StepStatus
import json
original = WorkflowState(
@@ -484,7 +484,7 @@ class TestParseInput:
def test_parse_input_validates_url_field(self):
"""ParseInput 必须包含 url 字段,且 min_length=1"""
from kb_pipeline.schemas import ParseInput
from kb_workflow.pipeline.schemas import ParseInput
# 缺少 url
with pytest.raises(ValidationError) as exc_info:
@@ -501,14 +501,14 @@ class TestParseInput:
def test_parse_input_content_id_default_empty(self):
"""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")
assert input_data.content_id == ""
def test_parse_input_platform_default_empty(self):
"""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")
assert input_data.platform == ""
@@ -519,7 +519,7 @@ class TestParseOutput:
def test_parse_output_inherits_step_output(self):
"""ParseOutput 继承 StepOutput 的 success 和 error_message"""
from kb_pipeline.schemas import ParseOutput, StepOutput
from kb_workflow.pipeline.schemas import ParseOutput, StepOutput
output = ParseOutput(
success=True,
@@ -536,7 +536,7 @@ class TestParseOutput:
def test_parse_output_has_required_fields(self):
"""ParseOutput 包含所有必需字段"""
from kb_pipeline.schemas import ParseOutput
from kb_workflow.pipeline.schemas import ParseOutput
output = ParseOutput(
success=True,
@@ -555,7 +555,7 @@ class TestParseOutput:
def test_parse_output_with_error(self):
"""ParseOutput 可以表示失败状态"""
from kb_pipeline.schemas import ParseOutput
from kb_workflow.pipeline.schemas import ParseOutput
output = ParseOutput(
success=False,
+12 -12
View File
@@ -15,7 +15,7 @@ import pytest
from pathlib import Path
from datetime import datetime
from kb_pipeline.schemas import WorkflowState, StepRecord, StepStatus
from kb_workflow.pipeline.schemas import WorkflowState, StepRecord, StepStatus
class TestWorkflowStateIsStepCompleted:
@@ -157,7 +157,7 @@ class TestAtomicWrite:
def test_save_creates_state_file(self, tmp_path):
"""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)
state = WorkflowState(
@@ -173,7 +173,7 @@ class TestAtomicWrite:
def test_save_creates_parent_directories(self, tmp_path):
"""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"
manager = WorkflowStateManager(content_dir=nested_dir)
@@ -190,7 +190,7 @@ class TestAtomicWrite:
def test_save_rotates_backup(self, tmp_path):
"""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)
state1 = WorkflowState(
@@ -228,7 +228,7 @@ class TestAtomicWrite:
def test_no_temp_files_remain_after_save(self, tmp_path):
"""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)
state = WorkflowState(
@@ -248,7 +248,7 @@ class TestBackupFallback:
def test_load_returns_state_for_valid_file(self, tmp_path):
"""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)
original = WorkflowState(
@@ -266,7 +266,7 @@ class TestBackupFallback:
def test_load_returns_none_for_missing_file(self, tmp_path):
"""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)
loaded = manager.load()
@@ -275,7 +275,7 @@ class TestBackupFallback:
def test_load_falls_back_to_backup_on_corruption(self, tmp_path):
"""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)
state_file = tmp_path / "workflow_state.json"
@@ -312,7 +312,7 @@ class TestBackupFallback:
def test_load_from_backup_when_main_missing(self, tmp_path):
"""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)
state_file = tmp_path / "workflow_state.json"
@@ -347,7 +347,7 @@ class TestStateManagerGetOrCreate:
def test_get_or_create_returns_existing_state(self, tmp_path):
"""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)
@@ -372,7 +372,7 @@ class TestStateManagerGetOrCreate:
def test_get_or_create_creates_new_state(self, tmp_path):
"""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)
@@ -390,7 +390,7 @@ class TestStateManagerGetOrCreate:
def test_get_or_create_is_idempotent(self, tmp_path):
"""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)