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
+59 -59
View File
@@ -12,8 +12,8 @@ from unittest.mock import AsyncMock, MagicMock, patch, mock_open
from pydantic import ValidationError
# 测试将在实现后导入
# from kb_pipeline.schemas import ASRInput, ASROutput
# from kb_pipeline.processors.asr import ASRProcessor
# from kb_workflow.pipeline.schemas import ASRInput, ASROutput
# from kb_workflow.pipeline.processors.asr import ASRProcessor
# =============================================================================
@@ -25,7 +25,7 @@ class TestASRInputSchema:
def test_asr_input_inherits_step_input(self):
"""ASRInput 应继承 StepInput 的字段"""
from kb_pipeline.schemas import ASRInput, StepInput
from kb_workflow.pipeline.schemas import ASRInput, StepInput
# 创建实例
input_data = ASRInput(
@@ -42,7 +42,7 @@ class TestASRInputSchema:
def test_asr_input_has_video_path(self):
"""ASRInput 必须包含 video_path 字段"""
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.schemas import ASRInput
input_data = ASRInput(
content_id="BV1xx",
@@ -56,7 +56,7 @@ class TestASRInputSchema:
def test_asr_input_content_type_default(self):
"""ASRInput content_type 默认为 'video'"""
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.schemas import ASRInput
input_data = ASRInput(
content_id="BV1xx",
@@ -73,7 +73,7 @@ class TestASROutputSchema:
def test_asr_output_inherits_step_output(self):
"""ASROutput 应继承 StepOutput 的字段"""
from kb_pipeline.schemas import ASROutput, StepOutput
from kb_workflow.pipeline.schemas import ASROutput, StepOutput
output = ASROutput(
success=True,
@@ -90,7 +90,7 @@ class TestASROutputSchema:
def test_asr_output_tool_literal(self):
"""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"]:
@@ -107,7 +107,7 @@ class TestASROutputSchema:
def test_asr_output_tool_invalid(self):
"""ASROutput tool 字段无效值应抛出 ValidationError"""
from kb_pipeline.schemas import ASROutput
from kb_workflow.pipeline.schemas import ASROutput
with pytest.raises(ValidationError):
ASROutput(
@@ -122,7 +122,7 @@ class TestASROutputSchema:
def test_asr_output_has_required_fields(self):
"""ASROutput 必须包含所有必需字段"""
from kb_pipeline.schemas import ASROutput
from kb_workflow.pipeline.schemas import ASROutput
output = ASROutput(
success=True,
@@ -143,7 +143,7 @@ class TestASROutputSchema:
def test_asr_output_speakers_optional(self):
"""ASROutput speakers 字段是可选的"""
from kb_pipeline.schemas import ASROutput
from kb_workflow.pipeline.schemas import ASROutput
# 无 speakers
output1 = ASROutput(
@@ -180,18 +180,18 @@ class TestASRProcessorInit:
def test_asr_processor_init_success(self, mock_settings):
"""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()
assert processor is not None
assert processor.STEP_NAME == "asr"
def test_asr_processor_with_state_manager(self, mock_settings, mock_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)
assert processor.state_manager is mock_state_manager
@@ -202,13 +202,13 @@ class TestOfficialSubtitle:
@pytest.mark.asyncio
async def test_official_subtitle_found(self, mock_settings, sample_subtitle_response, sample_subtitle_content, tmp_path):
"""_try_official_subtitle 找到字幕时返回 ASROutput"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
# Mock API 调用
@@ -238,13 +238,13 @@ class TestOfficialSubtitle:
@pytest.mark.asyncio
async def test_official_subtitle_not_found(self, mock_settings, tmp_path):
"""_try_official_subtitle 无字幕时返回 None"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
# Mock API 调用 - 无字幕
@@ -272,13 +272,13 @@ class TestOfficialSubtitle:
@pytest.mark.asyncio
async def test_process_tries_subtitle_first(self, mock_settings, tmp_path):
"""process() 方法优先尝试官方字幕"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
# Mock _try_official_subtitle 返回成功
@@ -304,8 +304,8 @@ class TestWhisperBasic:
@pytest.mark.asyncio
async def test_whisper_basic(self, mock_settings, mock_whisper_path, tmp_path):
"""_run_whisper 调用成功返回 ASROutput"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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")
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()
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:
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")
input_data = ASRInput(
@@ -344,8 +344,8 @@ class TestWhisperBasic:
@pytest.mark.asyncio
async def test_temp_file_cleanup(self, mock_settings, mock_whisper_path, tmp_path):
"""临时音频文件处理后自动清理"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
video_path.write_text("fake video")
@@ -353,14 +353,14 @@ class TestWhisperBasic:
audio_path = tmp_path / "test_audio.wav"
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()
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:
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:
mock_parse.return_value = MagicMock(
success=True,
@@ -387,16 +387,16 @@ class TestWhisperBasic:
@pytest.mark.asyncio
async def test_whisper_path_not_found(self, mock_settings, tmp_path):
"""Whisper 路径不存在时抛出 RuntimeError"""
from kb_pipeline.processors.asr import ASRProcessor, WhisperNotFoundError
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor, WhisperNotFoundError
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
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(
content_id="BV1xx",
platform="bilibili",
@@ -414,20 +414,20 @@ class TestDiarization:
@pytest.mark.asyncio
async def test_no_hf_token_uses_basic_whisper(self, mock_settings, mock_whisper_path, tmp_path):
"""HF_TOKEN 未配置时使用普通 Whisper"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
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:
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")
with patch.object(processor, "_parse_whisper_output") as mock_parse:
@@ -456,13 +456,13 @@ class TestDiarization:
@pytest.mark.asyncio
async def test_diarization_with_hf_token(self, mock_settings_with_hf_token, mock_whisper_path, tmp_path):
"""HF_TOKEN 配置时自动启用说话者分离"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
with patch.object(processor, "_run_whisper_with_diarization") as mock_diarization:
@@ -493,13 +493,13 @@ class TestDiarization:
@pytest.mark.asyncio
async def test_diarization_result_has_speaker(self, mock_settings_with_hf_token, mock_whisper_path, tmp_path):
"""说话者分离结果包含 speaker 字段"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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()
with patch.object(processor, "_run_whisper_basic") as mock_basic:
@@ -513,12 +513,12 @@ class TestDiarization:
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 = {
"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 = [
{"start": 0.0, "end": 3.0, "text": "hello", "speaker": "SPEAKER_00"}
]
@@ -538,8 +538,8 @@ class TestStateRecording:
@pytest.mark.asyncio
async def test_state_recording_success(self, mock_settings, mock_state_manager, tmp_path):
"""ASR 成功时状态记录为 completed"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput, WorkflowState
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput, WorkflowState
video_path = tmp_path / "test.mp4"
video_path.write_text("fake video")
@@ -547,7 +547,7 @@ class TestStateRecording:
# 创建 mock state
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)
mock_state_manager.load.return_value = mock_state
@@ -575,15 +575,15 @@ class TestStateRecording:
@pytest.mark.asyncio
async def test_state_recording_failure(self, mock_settings, mock_state_manager, tmp_path):
"""ASR 失败时状态记录为 failed"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput, WorkflowState
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput, WorkflowState
video_path = tmp_path / "test.mp4"
video_path.write_text("fake video")
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)
mock_state_manager.load.return_value = mock_state
@@ -609,13 +609,13 @@ class TestStateRecording:
@pytest.mark.asyncio
async def test_state_no_manager(self, mock_settings, tmp_path):
"""无 state_manager 时不记录状态"""
from kb_pipeline.processors.asr import ASRProcessor
from kb_pipeline.schemas import ASRInput
from kb_workflow.pipeline.processors.asr import ASRProcessor
from kb_workflow.pipeline.schemas import ASRInput
video_path = tmp_path / "test.mp4"
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)
with patch.object(processor, "_try_official_subtitle") as mock_sub: