test(15-00): add test infrastructure placeholders for Phase 15
创建了测试基础设施,为后续计划提供测试框架。 ## 创建的文件 1. **tests/test_processors/test_srt.py** - SRTProcessor 配置集成测试占位符 - TestConfigIntegration 测试类 - 5 个测试占位符(pytest.skip) 2. **tests/test_backward_compat.py** - 向后兼容性测试 - TestBackwardCompatibility 测试类 - 部分测试已实现(来自计划 15-01) - 部分测试占位符(pytest.skip) 3. **tests/conftest.py** - 添加共享 fixtures - temp_env_vars fixture:临时环境变量管理 - mock_kbsettings fixture:Mock KBSettings 实例 - 添加 os 模块导入 ## 偏离说明 计划要求所有测试都是占位符(pytest.skip),但实际上: - test_config.py 已由计划 15-01 实现完整测试 - test_backward_compat.py 有部分实现 原因:计划 15-01 在 15-00 之前执行,导致测试不是占位符。 ## 验证 - pytest 发现所有新增测试类和测试函数 - 测试文件结构符合项目规范 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""共享测试 fixtures 和配置"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from unittest.mock import Mock
|
||||
from pathlib import Path
|
||||
@@ -182,3 +183,54 @@ def long_subtitle_segments():
|
||||
text=f"这是第 {i + 1} 句字幕内容。" * 5
|
||||
))
|
||||
return segments
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Phase 15 Fixtures: 配置集成测试
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def temp_env_vars(monkeypatch):
|
||||
"""临时环境变量 fixture,测试后自动清理"""
|
||||
original_env = {}
|
||||
saved_vars = {}
|
||||
|
||||
def _set_env(env_dict):
|
||||
nonlocal original_env, saved_vars
|
||||
# 保存原始值
|
||||
for key in env_dict.keys():
|
||||
original_env[key] = os.environ.get(key)
|
||||
# 设置新值
|
||||
for key, value in env_dict.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
saved_vars[key] = value
|
||||
|
||||
yield _set_env
|
||||
|
||||
# 清理:恢复原始环境变量
|
||||
for key, original_value in original_env.items():
|
||||
if original_value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = original_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_kbsettings():
|
||||
"""Mock KBSettings fixture,用于隔离测试环境"""
|
||||
from kb_workflow.pipeline.config import KBSettings
|
||||
from kb_workflow.pipeline.config import get_settings
|
||||
|
||||
mock_settings = MagicMock(spec=KBSettings)
|
||||
mock_settings.enable_semantic_segmentation = True
|
||||
mock_settings.base_path = "/tmp/kb"
|
||||
mock_settings.archive_path = "/tmp/archive"
|
||||
mock_settings.hf_token = MagicMock()
|
||||
mock_settings.hf_token.get_secret_value.return_value = ""
|
||||
mock_settings.zhihu_cookie = MagicMock()
|
||||
mock_settings.zhihu_cookie.get_secret_value.return_value = ""
|
||||
|
||||
return mock_settings
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Backward Compatibility Tests
|
||||
|
||||
Tests to ensure that configuration changes don't break existing functionality:
|
||||
- Rule-based segmentation (SemanticSegmenter) works unchanged (CONFIG-02)
|
||||
- AnalyzeProcessor is not affected by SRTProcessor config changes (CONFIG-03)
|
||||
- Existing API calls remain compatible
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from pathlib import Path
|
||||
from kb_workflow.srt.parser import Segment
|
||||
from kb_workflow.srt.segmenter import SemanticSegmenter
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test Fixtures
|
||||
# =============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def sample_segments():
|
||||
"""Sample subtitle segments for testing"""
|
||||
return [
|
||||
Segment(1, 0.0, 5.0, "第一句内容"),
|
||||
Segment(2, 5.0, 10.0, "第二句内容"),
|
||||
Segment(3, 10.0, 15.0, "第三句内容"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def long_text_segments():
|
||||
"""Segments with long text that triggers length-based splitting"""
|
||||
text = "这是一段很长的文本内容" * 50 # ~500 characters
|
||||
return [
|
||||
Segment(1, 0.0, 5.0, text[:250]),
|
||||
Segment(2, 5.0, 10.0, text[:250]),
|
||||
Segment(3, 10.0, 15.0, text[:250]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gap_segments():
|
||||
"""Segments with time gaps > 3 seconds"""
|
||||
return [
|
||||
Segment(1, 0.0, 5.0, "第一段落内容"),
|
||||
Segment(2, 10.0, 15.0, "第二段落内容"), # 5 second gap
|
||||
Segment(3, 15.0, 20.0, "继续第二段落"),
|
||||
]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Task 1: Rule-based Segmentation Backward Compatibility (CONFIG-02)
|
||||
# =============================================================================
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Test backward compatibility for rule-based segmentation (CONFIG-02)"""
|
||||
|
||||
def test_rule_based_segmentation_unchanged(self, sample_segments):
|
||||
"""Test 1: enable_semantic_segmentation=False 使用规则分段器"""
|
||||
from kb_workflow.pipeline.processors.srt import SRTProcessor
|
||||
from kb_workflow.srt.semantic_engine import SemanticSegmentationEngine
|
||||
|
||||
# Create SRTProcessor with semantic segmentation disabled
|
||||
processor = SRTProcessor(
|
||||
state_manager=None,
|
||||
enable_semantic_segmentation=False
|
||||
)
|
||||
|
||||
# Verify it uses SemanticSegmenter (rule-based)
|
||||
assert isinstance(processor.segmenter, SemanticSegmenter), \
|
||||
"Should use SemanticSegmenter when enable_semantic_segmentation=False"
|
||||
|
||||
# Verify it does NOT use SemanticSegmentationEngine
|
||||
assert not isinstance(processor.segmenter, SemanticSegmentationEngine), \
|
||||
"Should NOT use SemanticSegmentationEngine when enable_semantic_segmentation=False"
|
||||
|
||||
# Verify segment() returns plain text list (not VideoSegment objects)
|
||||
result = processor.segmenter.segment(sample_segments)
|
||||
assert isinstance(result, list), "segment() should return a list"
|
||||
assert len(result) > 0, "Should have at least one paragraph"
|
||||
assert all(isinstance(para, str) for para in result), \
|
||||
"Rule-based segmenter should return plain text strings, not VideoSegment objects"
|
||||
|
||||
def test_rule_based_no_llm_calls(self, sample_segments):
|
||||
"""Test 2: 规则分段不调用 LLM,无网络开销"""
|
||||
from kb_workflow.pipeline.processors.srt import SRTProcessor
|
||||
|
||||
# Mock SimpleAIClient to detect any LLM calls
|
||||
with patch('kb_workflow.deep_analyzer.SimpleAIClient') as mock_client:
|
||||
# Create processor with rule-based segmentation
|
||||
processor = SRTProcessor(
|
||||
state_manager=None,
|
||||
enable_semantic_segmentation=False
|
||||
)
|
||||
|
||||
# Call segment method
|
||||
result = processor.segmenter.segment(sample_segments)
|
||||
|
||||
# Verify SimpleAIClient was NOT called
|
||||
# (rule-based segmentation should not initialize AI client)
|
||||
assert not mock_client.called, \
|
||||
"Rule-based segmentation should NOT call SimpleAIClient"
|
||||
|
||||
# Verify we still get results without LLM
|
||||
assert len(result) > 0, "Should have results without LLM"
|
||||
|
||||
def test_rule_based_thresholds_unchanged(self, gap_segments, long_text_segments):
|
||||
"""Test 3: 规则分段的分段逻辑(时间/长度阈值)未改变"""
|
||||
segmenter = SemanticSegmenter(
|
||||
gap_threshold=3.0, # Phase 12 default
|
||||
max_paragraph_chars=500 # Phase 12 default
|
||||
)
|
||||
|
||||
# Test 1: Time gap threshold (3 seconds)
|
||||
result = segmenter.segment(gap_segments)
|
||||
# Should split into 2 paragraphs due to 5-second gap
|
||||
assert len(result) == 2, \
|
||||
f"Should split into 2 paragraphs due to time gap, got {len(result)}"
|
||||
assert "第一段落内容" in result[0]
|
||||
assert "第二段落内容" in result[1]
|
||||
|
||||
# Test 2: Character length threshold (500 chars)
|
||||
result = segmenter.segment(long_text_segments)
|
||||
# Each segment is ~250 chars, so with 3 segments we should get splits
|
||||
# First segment: 250 chars (no split)
|
||||
# Second segment: 250 + 250 = 500 chars (might split depending on exact count)
|
||||
assert len(result) >= 1, "Should have at least one paragraph"
|
||||
assert all(isinstance(para, str) for para in result), \
|
||||
"All results should be plain text strings"
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 3")
|
||||
def test_existing_api_compatibility(self):
|
||||
"""Test 4: 不传 enable_semantic_segmentation 参数时使用配置默认值"""
|
||||
# This test will be implemented in Task 3
|
||||
pass
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 3")
|
||||
def test_explicit_false_overrides_config(self):
|
||||
"""Test 5: 显式传 False 时覆盖配置,使用规则分段"""
|
||||
# This test will be implemented in Task 3
|
||||
pass
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 3")
|
||||
def test_migration_path(self):
|
||||
"""Test 6: 验证用户可以通过两种方式启用语义分段"""
|
||||
# This test will be implemented in Task 3
|
||||
pass
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 2")
|
||||
def test_analyze_processor_unaffected(self):
|
||||
"""Test 7: AnalyzeProcessor 不受 KBSettings.enable_semantic_segmentation 影响"""
|
||||
# This test will be implemented in Task 2
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Task 2: AnalyzeProcessor Independence (CONFIG-03)
|
||||
# =============================================================================
|
||||
|
||||
class TestAnalyzeProcessorIndependence:
|
||||
"""Test that AnalyzeProcessor is independent of SRTProcessor configuration"""
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 2")
|
||||
def test_analyze_processor_unaffected_by_config(self):
|
||||
"""Test: AnalyzeProcessor 不受 KBSettings.enable_semantic_segmentation 影响"""
|
||||
# This test will be implemented in Task 2
|
||||
pass
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 2")
|
||||
def test_analyze_processor_normal_initialization(self):
|
||||
"""Test: AnalyzeProcessor 正常初始化和处理"""
|
||||
# This test will be implemented in Task 2
|
||||
pass
|
||||
|
||||
@pytest.mark.skipif(True, reason="Not implemented yet - will implement in Task 2")
|
||||
def test_analyze_processor_output_unchanged(self):
|
||||
"""Test: AnalyzeProcessor 输出格式未改变"""
|
||||
# This test will be implemented in Task 2
|
||||
pass
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Tests for SRTProcessor and configuration integration.
|
||||
|
||||
Test coverage:
|
||||
- Configuration integration (CONFIG-01)
|
||||
- Backward compatibility (CONFIG-02)
|
||||
- Output format verification (OUTPUT-01, OUTPUT-02, OUTPUT-03)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestConfigIntegration:
|
||||
"""Tests for SRTProcessor configuration integration (CONFIG-01, CONFIG-02)"""
|
||||
|
||||
def test_srt_processor_reads_from_kbsettings(self):
|
||||
"""Test SRTProcessor reads enable_semantic_segmentation from KBSettings"""
|
||||
pytest.skip("等待实现: 验证从 KBSettings 读取配置")
|
||||
|
||||
def test_srt_processor_constructor_overrides_config(self):
|
||||
"""Test constructor parameter overrides KBSettings"""
|
||||
pytest.skip("等待实现: 验证构造函数参数优先级")
|
||||
|
||||
def test_config_disabled_uses_rule_based(self):
|
||||
"""Test disabled config uses rule-based segmentation (CONFIG-02)"""
|
||||
pytest.skip("等待实现: 验证禁用时使用规则分段")
|
||||
|
||||
def test_output_format_with_semantic_enabled(self):
|
||||
"""Test output format with semantic segmentation enabled (OUTPUT-01)"""
|
||||
pytest.skip("等待实现: 验证启用时输出 H2 标题")
|
||||
|
||||
def test_output_format_with_semantic_disabled(self):
|
||||
"""Test output format with semantic segmentation disabled (OUTPUT-03)"""
|
||||
pytest.skip("等待实现: 验证禁用时输出\"## 内容\"")
|
||||
Reference in New Issue
Block a user