feat(04-02): implement DownloadProcessor with metadata fetching

TDD GREEN phase:
- DownloadProcessor inherits from BaseProcessor
- Fetches metadata from Bilibili and Zhihu adapters
- Saves metadata to content_dir as JSON
- Records state on success/failure
- Skips step if already completed (idempotency)
- All metadata tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 23:22:48 +08:00
parent 567bd32c94
commit fc6fba76b2
2 changed files with 329 additions and 21 deletions
+31 -21
View File
@@ -161,6 +161,7 @@ class TestDownloadProcessorMetadata:
"url": "https://www.zhihu.com/question/123/answer/456",
"content_markdown": "Test content"
})
adapter.fetch_content = AsyncMock(return_value="---\ntitle: Test\n---\nTest content")
adapter.get_output_filename = MagicMock(
return_value="zhihu_456_metadata.json"
)
@@ -189,11 +190,13 @@ class TestDownloadProcessorMetadata:
content_type="video"
)
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
assert result.success is True
assert result.title == "Test Video Title"
@@ -219,11 +222,13 @@ class TestDownloadProcessorMetadata:
content_type="answer"
)
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_zhihu_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_zhihu_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
assert result.success is True
assert result.title == "Test Answer Title"
@@ -253,11 +258,13 @@ class TestDownloadProcessorMetadata:
content_type="video"
)
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
assert result.success is True
@@ -281,6 +288,7 @@ class TestDownloadProcessorMetadata:
side_effect=NetworkError(
step_name="download",
input_summary="BV1xx",
status_code=503,
cause=Exception("Network timeout")
)
)
@@ -297,14 +305,16 @@ class TestDownloadProcessorMetadata:
content_type="video"
)
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
with patch("kb_pipeline.processors.base.get_settings") as mock_settings:
mock_settings.return_value = MagicMock()
with patch.object(
DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter
):
processor = DownloadProcessor(state_manager=mock_state_manager)
result = await processor.process(input_data)
assert result.success is False
assert "Network timeout" in result.error_message
assert "503" in result.error_message # NetworkError message includes status code
# Verify failure was recorded
mock_state.mark_step_failed.assert_called_once()