This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/tests/test_processors/test_analyze.py
T
starmen1265 c9ca1b538d 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>
2026-03-17 00:17:10 +08:00

433 lines
17 KiB
Python

"""Tests for AnalyzeProcessor and AnalyzeInput/AnalyzeOutput schemas.
Test coverage:
- AnalyzeInput schema validation
- AnalyzeOutput schema validation
- AnalyzeProcessor DeepAnalyzer integration
- AnalyzeProcessor state recording
"""
import pytest
import json
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
from pathlib import Path
from dataclasses import dataclass, field
from typing import List
from kb_workflow.pipeline.schemas import AnalyzeInput, AnalyzeOutput
# Mock AnalysisResult for testing
@dataclass
class MockAnalysisResult:
"""Mock AnalysisResult for testing."""
summary: str = "Test summary"
main_topics: List[str] = field(default_factory=lambda: ["Topic 1", "Topic 2"])
key_insights: List[str] = field(default_factory=lambda: ["Insight 1"])
recommended_tags: List[str] = field(default_factory=lambda: ["tag1", "tag2"])
full_analysis: str = "## Summary\n\nTest analysis content"
original_text: str = ""
class TestAnalyzeInputSchema:
"""Tests for AnalyzeInput schema validation."""
def test_schema_analyze_input_validates_required_fields(self):
"""Test 1: AnalyzeInput validates required fields"""
input_data = AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file="/tmp/content/bilibili_BV1xx_metadata.json",
content_file="/tmp/content/bilibili_BV1xx.json",
content_type="video",
output_file="/tmp/content/bilibili_BV1xx_analysis.md"
)
assert input_data.content_id == "BV1xx"
assert input_data.platform == "bilibili"
assert input_data.metadata_file == "/tmp/content/bilibili_BV1xx_metadata.json"
assert input_data.content_file == "/tmp/content/bilibili_BV1xx.json"
assert input_data.content_type == "video"
assert input_data.output_file == "/tmp/content/bilibili_BV1xx_analysis.md"
def test_schema_analyze_input_requires_metadata_file(self):
"""AnalyzeInput should require metadata_file"""
with pytest.raises(Exception): # ValidationError from pydantic
AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
content_file="/tmp/content/bilibili_BV1xx.json",
content_type="video",
output_file="/tmp/content/bilibili_BV1xx_analysis.md"
# Missing metadata_file
)
def test_schema_analyze_input_requires_content_file(self):
"""AnalyzeInput should require content_file"""
with pytest.raises(Exception): # ValidationError from pydantic
AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file="/tmp/content/bilibili_BV1xx_metadata.json",
content_type="video",
output_file="/tmp/content/bilibili_BV1xx_analysis.md"
# Missing content_file
)
def test_schema_analyze_input_requires_content_type(self):
"""AnalyzeInput should require content_type"""
with pytest.raises(Exception): # ValidationError from pydantic
AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file="/tmp/content/bilibili_BV1xx_metadata.json",
content_file="/tmp/content/bilibili_BV1xx.json",
output_file="/tmp/content/bilibili_BV1xx_analysis.md"
# Missing content_type
)
def test_schema_analyze_input_requires_output_file(self):
"""AnalyzeInput should require output_file"""
with pytest.raises(Exception): # ValidationError from pydantic
AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file="/tmp/content/bilibili_BV1xx_metadata.json",
content_file="/tmp/content/bilibili_BV1xx.json",
content_type="video"
# Missing output_file
)
class TestAnalyzeOutputSchema:
"""Tests for AnalyzeOutput schema validation."""
def test_schema_analyze_output_validates_required_fields(self):
"""Test 2: AnalyzeOutput validates required fields"""
output = AnalyzeOutput(
success=True,
analysis_file="/tmp/content/bilibili_BV1xx_analysis.md",
summary="This is a test summary",
main_topics=["Topic 1", "Topic 2"],
key_insights=["Insight 1", "Insight 2"],
tags=["tag1", "tag2"],
stats={"segments_count": 10}
)
assert output.success is True
assert output.analysis_file == "/tmp/content/bilibili_BV1xx_analysis.md"
assert output.summary == "This is a test summary"
assert output.main_topics == ["Topic 1", "Topic 2"]
assert output.key_insights == ["Insight 1", "Insight 2"]
assert output.tags == ["tag1", "tag2"]
assert output.stats == {"segments_count": 10}
def test_schema_analyze_output_handles_video_content_type(self):
"""Test 3: AnalyzeOutput handles video content type"""
output = AnalyzeOutput(
success=True,
analysis_file="/tmp/content/bilibili_BV1xx_analysis.md",
summary="Video summary",
main_topics=["Topic 1"],
key_insights=["Insight 1"],
tags=["video", "test"],
stats={"duration": 300, "segments": 15}
)
assert output.success is True
assert "duration" in output.stats
assert output.stats["segments"] == 15
def test_schema_analyze_output_handles_article_content_type(self):
"""Test 3: AnalyzeOutput handles article content type"""
output = AnalyzeOutput(
success=True,
analysis_file="/tmp/content/zhihu_123_analysis.md",
summary="Article summary",
main_topics=["Topic 1"],
key_insights=[],
tags=["article", "zhihu"],
stats={"word_count": 2000}
)
assert output.success is True
assert "word_count" in output.stats
def test_schema_analyze_output_handles_failure(self):
"""AnalyzeOutput should handle failure case"""
output = AnalyzeOutput(
success=False,
error_message="AI API error",
analysis_file="",
summary="",
main_topics=[],
key_insights=[],
tags=[],
stats={}
)
assert output.success is False
assert output.error_message == "AI API error"
class TestAnalyzeProcessorIntegration:
"""Tests for AnalyzeProcessor DeepAnalyzer integration."""
@pytest.fixture
def mock_analysis_result(self):
"""Create a mock AnalysisResult."""
return MockAnalysisResult()
@pytest.fixture
def mock_state_manager(self):
"""Create a mock WorkflowStateManager."""
manager = MagicMock()
manager.load = MagicMock(return_value=None)
manager.save = MagicMock()
return manager
@pytest.fixture
def sample_metadata(self, tmp_path):
"""Create sample metadata file."""
metadata = {
"title": "Test Video Title",
"author": "Test Author",
"pubtime": "2024-01-01",
"duration": 300,
"bvid": "BV1xx",
"platform": "bilibili",
"url": "https://www.bilibili.com/video/BV1xx"
}
metadata_file = tmp_path / "bilibili_BV1xx_metadata.json"
metadata_file.write_text(json.dumps(metadata, ensure_ascii=False), encoding="utf-8")
return str(metadata_file)
@pytest.fixture
def sample_content(self, tmp_path):
"""Create sample content file."""
content = {
"text": "This is the full transcript of the video."
}
content_file = tmp_path / "bilibili_BV1xx.json"
content_file.write_text(json.dumps(content, ensure_ascii=False), encoding="utf-8")
return str(content_file)
@pytest.mark.asyncio
async def test_analyze_loads_metadata_and_content_from_files(
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
):
"""Test 1: AnalyzeProcessor loads metadata and content from files"""
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
from kb_workflow.pipeline.schemas import AnalyzeInput
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
input_data = AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file=sample_metadata,
content_file=sample_content,
content_type="video",
output_file=output_file
)
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
processor = AnalyzeProcessor(state_manager=mock_state_manager)
# Mock _run_analysis to return our mock result
async def mock_run_analysis(metadata, content, content_type):
# Verify metadata was loaded correctly
assert "title" in metadata
assert metadata["title"] == "Test Video Title"
# Verify content was loaded correctly
assert "text" in content
return mock_analysis_result
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
with patch.object(processor, '_write_analysis_report'):
result = await processor.process(input_data)
assert result.success is True
@pytest.mark.asyncio
async def test_analyze_calls_deep_analyzer_with_correct_parameters(
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
):
"""Test 2: AnalyzeProcessor calls DeepAnalyzer.analyze() with correct parameters"""
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
from kb_workflow.pipeline.schemas import AnalyzeInput
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
input_data = AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file=sample_metadata,
content_file=sample_content,
content_type="video",
output_file=output_file
)
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
processor = AnalyzeProcessor(state_manager=mock_state_manager)
# Track what parameters were passed
captured_params = {}
async def mock_run_analysis(metadata, content, content_type):
captured_params['metadata'] = metadata
captured_params['content'] = content
captured_params['content_type'] = content_type
return mock_analysis_result
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
with patch.object(processor, '_write_analysis_report'):
result = await processor.process(input_data)
assert result.success is True
# Verify content_type was passed correctly
assert captured_params['content_type'] == "video"
@pytest.mark.asyncio
async def test_analyze_saves_analysis_report_as_markdown(
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
):
"""Test 3: AnalyzeProcessor saves analysis report as Markdown"""
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
from kb_workflow.pipeline.schemas import AnalyzeInput
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
input_data = AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file=sample_metadata,
content_file=sample_content,
content_type="video",
output_file=output_file
)
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
processor = AnalyzeProcessor(state_manager=mock_state_manager)
async def mock_run_analysis(metadata, content, content_type):
return mock_analysis_result
def mock_write_report(output_path, metadata, result, content_type):
# Write a simple markdown file with frontmatter
with open(output_path, 'w', encoding='utf-8') as f:
f.write("---\ntitle: Test\n---\n\n## Summary\n\nTest analysis content")
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
with patch.object(processor, '_write_analysis_report', side_effect=mock_write_report):
result = await processor.process(input_data)
assert result.success is True
assert result.analysis_file == output_file
# Verify Markdown file was created
output_path = Path(output_file)
assert output_path.exists()
# Verify content includes frontmatter and analysis
content = output_path.read_text(encoding="utf-8")
assert content.startswith("---") # YAML frontmatter
assert "##" in content # Markdown headers
@pytest.mark.asyncio
async def test_analyze_records_state_on_success(
self, mock_analysis_result, mock_state_manager, sample_metadata, sample_content, tmp_path
):
"""Test 4: AnalyzeProcessor records state on success"""
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
from kb_workflow.pipeline.schemas import AnalyzeInput, WorkflowState
# Create a mock state that will be updated
mock_state = MagicMock(spec=WorkflowState)
mock_state.is_step_completed = MagicMock(return_value=False)
mock_state_manager.load.return_value = mock_state
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
input_data = AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file=sample_metadata,
content_file=sample_content,
content_type="video",
output_file=output_file
)
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
processor = AnalyzeProcessor(state_manager=mock_state_manager)
async def mock_run_analysis(metadata, content, content_type):
return mock_analysis_result
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis):
with patch.object(processor, '_write_analysis_report'):
result = await processor.process(input_data)
assert result.success is True
# Verify state was recorded
mock_state.mark_step_completed.assert_called_once()
call_args = mock_state.mark_step_completed.call_args
assert call_args[0][0] == "analyze" # step_name
mock_state_manager.save.assert_called_once_with(mock_state)
@pytest.mark.asyncio
async def test_analyze_records_state_on_failure(
self, mock_state_manager, sample_metadata, sample_content, tmp_path
):
"""Test 5: AnalyzeProcessor records state on failure"""
from kb_workflow.pipeline.processors.analyze import AnalyzeProcessor
from kb_workflow.pipeline.schemas import AnalyzeInput, WorkflowState
# Create a mock state that will be updated
mock_state = MagicMock(spec=WorkflowState)
mock_state.is_step_completed = MagicMock(return_value=False)
mock_state_manager.load.return_value = mock_state
output_file = str(tmp_path / "bilibili_BV1xx_analysis.md")
input_data = AnalyzeInput(
content_id="BV1xx",
platform="bilibili",
metadata_file=sample_metadata,
content_file=sample_content,
content_type="video",
output_file=output_file
)
with patch("kb_workflow.pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
processor = AnalyzeProcessor(state_manager=mock_state_manager)
async def mock_run_analysis_error(metadata, content, content_type):
raise ValueError("AI API error")
with patch.object(processor, '_run_analysis', side_effect=mock_run_analysis_error):
result = await processor.process(input_data)
assert result.success is False
assert "AI API error" in result.error_message
# Verify failure was recorded
mock_state.mark_step_failed.assert_called_once()
call_args = mock_state.mark_step_failed.call_args
assert call_args[0][0] == "analyze" # step_name
mock_state_manager.save.assert_called_once_with(mock_state)
class TestAnalyzeProcessorModuleExport:
"""Tests for AnalyzeProcessor module export."""
def test_analyze_processor_importable_from_processors(self):
"""Test: AnalyzeProcessor can be imported from kb_workflow.pipeline.processors"""
from kb_workflow.pipeline.processors import AnalyzeProcessor
assert AnalyzeProcessor is not None
assert hasattr(AnalyzeProcessor, 'STEP_NAME')
assert AnalyzeProcessor.STEP_NAME == "analyze"