feat(09-01): integrate ZhihuCrawler into ZhihuAdapter
- Replace ZhihuPlaywrightExtractor with ZhihuCrawler - Add comment crawling with include_comments=True by default - Implement cookie expiration detection and QR code login trigger - Add idempotency check via IdempotencyChecker - Add comment formatting with "## 热门评论" section - Convert HTML content to Markdown using html2text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+345
-188
@@ -7,21 +7,24 @@ Tests cover:
|
||||
- Login page detection
|
||||
- AuthError handling
|
||||
|
||||
Wave 0 Test Scaffolding (Phase 09 - Pipeline Integration):
|
||||
- Mock fixtures for ZhihuCrawler CrawlResult
|
||||
- Test skeletons for integration with new crawler
|
||||
Phase 09 - Pipeline Integration:
|
||||
- ZhihuAdapter now uses ZhihuCrawler instead of ZhihuPlaywrightExtractor
|
||||
- Comments are included by default and appended to content
|
||||
- Cookie expiration triggers QR code login
|
||||
- Idempotency via IdempotencyChecker
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, PropertyMock
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from kb_workflow.pipeline.platforms.zhihu import ZhihuAdapter
|
||||
from kb_workflow.pipeline.errors import AuthError
|
||||
|
||||
# =============================================================================
|
||||
# Wave 0 Mock Fixtures for ZhihuCrawler Integration (Phase 09)
|
||||
# Mock Fixtures for ZhihuCrawler Integration (Phase 09)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@@ -127,6 +130,35 @@ def mock_crawl_article_result():
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_crawl_no_comments_result():
|
||||
"""Mock CrawlResult without comments."""
|
||||
from kb_workflow.crawler.models import (
|
||||
CrawlResult, AnswerData, AuthorInfo, ContentMetadata
|
||||
)
|
||||
|
||||
return CrawlResult(
|
||||
success=True,
|
||||
content_type="answer",
|
||||
data=AnswerData(
|
||||
id=789012,
|
||||
question_id=123456,
|
||||
question_title="Test Question No Comments",
|
||||
content="<p>Answer without comments.</p>",
|
||||
excerpt="Answer without comments.",
|
||||
metadata=ContentMetadata(
|
||||
author=AuthorInfo(name="Test Author", url_token="test-author"),
|
||||
voteup_count=100,
|
||||
comment_count=0,
|
||||
created_time=1705312800,
|
||||
updated_time=None
|
||||
)
|
||||
),
|
||||
comments=[],
|
||||
error=None
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_comments_list():
|
||||
"""Mock list of CommentData for testing comment formatting.
|
||||
@@ -198,8 +230,21 @@ def mock_crawl_failed_result():
|
||||
)
|
||||
|
||||
|
||||
class TestZhihuAdapterAnswerContent:
|
||||
"""Tests for ZhihuAdapter answer extraction"""
|
||||
@pytest.fixture
|
||||
def mock_idempotency_checker():
|
||||
"""Mock IdempotencyChecker for testing idempotent behavior."""
|
||||
mock = MagicMock()
|
||||
mock.is_processed.return_value = False
|
||||
return mock
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Basic Adapter Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestZhihuAdapterBasic:
|
||||
"""Tests for basic ZhihuAdapter functionality"""
|
||||
|
||||
def test_get_content_type_answer(self):
|
||||
"""Test content type detection for answer URLs"""
|
||||
@@ -234,32 +279,90 @@ class TestZhihuAdapterAnswerContent:
|
||||
assert adapter.get_output_filename("789012", "answer", "metadata.json") == "zhihu_789012_metadata.json"
|
||||
assert adapter.get_output_filename("123456", "zhuanlan", "content.md") == "zhihu_123456_content.md"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_answer(self, mock_zhihu_answer_result):
|
||||
"""Test fetching answer metadata"""
|
||||
def test_platform_constant(self):
|
||||
"""Test PLATFORM constant"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie" # Set cookie directly
|
||||
adapter._cookie = "test" # Avoid config loading
|
||||
assert adapter.PLATFORM == "zhihu"
|
||||
|
||||
with patch.object(adapter, '_extract_with_playwright', return_value=mock_zhihu_answer_result):
|
||||
metadata = await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
assert metadata["title"] == "Test Question Title"
|
||||
# =============================================================================
|
||||
# ZhihuCrawler Integration Tests (Phase 09)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestZhihuAdapterCrawlerIntegration:
|
||||
"""Tests for ZhihuAdapter integration with ZhihuCrawler.
|
||||
|
||||
These tests verify the adapter correctly uses the new ZhihuCrawler
|
||||
instead of ZhihuPlaywrightExtractor.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_answer(self, mock_crawl_answer_result):
|
||||
"""Test fetching answer metadata via ZhihuCrawler.
|
||||
|
||||
Verifies:
|
||||
- ZhihuCrawler.crawl_answer() is called with include_comments=True
|
||||
- Metadata is correctly extracted from CrawlResult
|
||||
- Output format matches expected adapter response
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_answer_result):
|
||||
metadata = await adapter.fetch_metadata("789012", "answer", source_url="https://www.zhihu.com/question/123456/answer/789012")
|
||||
|
||||
assert metadata["title"] == "Test Question Title for Pipeline Integration"
|
||||
assert metadata["author"] == "Test Author"
|
||||
assert metadata["type"] == "answer"
|
||||
assert metadata["voteup_count"] == 1234
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_content_answer(self, mock_zhihu_answer_result):
|
||||
"""Test fetching answer content as Markdown"""
|
||||
async def test_fetch_metadata_article(self, mock_crawl_article_result):
|
||||
"""Test fetching zhuanlan article metadata via ZhihuCrawler.
|
||||
|
||||
Verifies:
|
||||
- ZhihuCrawler.crawl_article() is called with correct URL
|
||||
- Metadata is correctly extracted from CrawlResult
|
||||
- Output format matches expected adapter response
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie" # Set cookie directly
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
with patch.object(adapter, '_extract_with_playwright', return_value=mock_zhihu_answer_result):
|
||||
content = await adapter.fetch_content("789012", "answer")
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_article_result):
|
||||
metadata = await adapter.fetch_metadata("123456", "zhuanlan")
|
||||
|
||||
assert "---" in content # YAML frontmatter
|
||||
assert metadata["title"] == "Test Article Title for Pipeline Integration"
|
||||
assert metadata["author"] == "Test Article Author"
|
||||
assert metadata["type"] == "zhuanlan"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_content_with_comments(self, mock_crawl_answer_result):
|
||||
"""Test fetching content with comments appended.
|
||||
|
||||
Verifies:
|
||||
- Content is fetched with include_comments=True
|
||||
- Comments are appended to content in correct Markdown format
|
||||
- Comment format: numbered list with author mention
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_answer_result):
|
||||
content = await adapter.fetch_content("789012", "answer", source_url="https://www.zhihu.com/question/123456/answer/789012")
|
||||
|
||||
# Verify YAML frontmatter
|
||||
assert "---" in content
|
||||
assert "title:" in content
|
||||
assert "Test Question Title" in content
|
||||
assert "This is the answer content" in content
|
||||
|
||||
# Verify main content
|
||||
assert "Test Question Title for Pipeline Integration" in content
|
||||
|
||||
# Verify comments section is appended
|
||||
assert "## 热门评论" in content
|
||||
assert "@Commenter1" in content
|
||||
assert "Great answer!" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_no_cookie_raises_auth_error(self):
|
||||
@@ -273,80 +376,230 @@ class TestZhihuAdapterAnswerContent:
|
||||
assert exc_info.value.auth_type == "zhihu_cookie"
|
||||
|
||||
|
||||
class TestZhihuAdapterZhuanlanContent:
|
||||
"""Tests for ZhihuAdapter zhuanlan (article) extraction"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_zhuanlan(self, mock_zhihu_zhuanlan_result):
|
||||
"""Test fetching zhuanlan metadata"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie" # Set cookie directly
|
||||
|
||||
with patch.object(adapter, '_extract_with_playwright', return_value=mock_zhihu_zhuanlan_result):
|
||||
metadata = await adapter.fetch_metadata("123456", "zhuanlan")
|
||||
|
||||
assert metadata["title"] == "Test Article Title"
|
||||
assert metadata["author"] == "Test Author"
|
||||
assert metadata["type"] == "zhuanlan"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_content_zhuanlan(self, mock_zhihu_zhuanlan_result):
|
||||
"""Test fetching zhuanlan content as Markdown"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie" # Set cookie directly
|
||||
|
||||
with patch.object(adapter, '_extract_with_playwright', return_value=mock_zhihu_zhuanlan_result):
|
||||
content = await adapter.fetch_content("123456", "zhuanlan")
|
||||
|
||||
assert "---" in content # YAML frontmatter
|
||||
assert "title:" in content
|
||||
assert "Test Article Title" in content
|
||||
assert "This is the article content" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detect_login_page_raises_auth_error(self):
|
||||
"""Test that login page detection raises AuthError"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "expired_cookie" # Set cookie directly
|
||||
|
||||
# Simulate login page response (empty content or login title)
|
||||
login_page_result = {
|
||||
"type": "answer",
|
||||
"title": "登录 - 知乎",
|
||||
"content": "",
|
||||
"content_markdown": "",
|
||||
}
|
||||
|
||||
with patch.object(adapter, '_extract_with_playwright', return_value=login_page_result):
|
||||
with pytest.raises(AuthError) as exc_info:
|
||||
await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
assert exc_info.value.auth_type == "zhihu_cookie"
|
||||
assert "过期" in str(exc_info.value.cause) or "expired" in str(exc_info.value.cause).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detect_short_content_raises_auth_error(self):
|
||||
"""Test that very short content (< 100 chars) raises AuthError"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
# Simulate response with very short content
|
||||
short_content_result = {
|
||||
"type": "answer",
|
||||
"title": "Some Title",
|
||||
"content": "x", # Very short content
|
||||
"content_markdown": "",
|
||||
}
|
||||
|
||||
with patch.object(adapter, '_extract_with_playwright', return_value=short_content_result):
|
||||
with pytest.raises(AuthError) as exc_info:
|
||||
await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
assert exc_info.value.auth_type == "zhihu_cookie"
|
||||
# =============================================================================
|
||||
# Cookie Expiration and Auto-Login Tests (Task 2)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestZhihuAdapterCookieHandling:
|
||||
"""Tests for cookie handling"""
|
||||
"""Tests for cookie handling and auto-login"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cookie_valid_returns_data(self, mock_crawl_answer_result):
|
||||
"""Test that valid cookie allows normal data retrieval"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "valid_cookie"
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_answer_result):
|
||||
metadata = await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
assert metadata["title"] == "Test Question Title for Pipeline Integration"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cookie_expired_triggers_login(self, mock_crawl_failed_result, mock_zhihu_login_result, mock_crawl_answer_result):
|
||||
"""Test that expired cookie triggers QR code login flow.
|
||||
|
||||
Verifies:
|
||||
- CrawlResult with success=False triggers login
|
||||
- ZhihuLogin.login_by_qrcode() is called
|
||||
- New cookie is used to retry the fetch
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "expired_cookie"
|
||||
|
||||
# First call fails (cookie expired), second succeeds after login
|
||||
crawl_results = [mock_crawl_failed_result, mock_crawl_answer_result]
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', side_effect=crawl_results):
|
||||
with patch.object(adapter, '_trigger_qrcode_login', return_value=mock_zhihu_login_result):
|
||||
metadata = await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
# Verify login was triggered and cookie was updated
|
||||
assert adapter._cookie == mock_zhihu_login_result.cookie_str
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_retry_success(self, mock_crawl_failed_result, mock_zhihu_login_result, mock_crawl_answer_result):
|
||||
"""Test that after successful login, retry succeeds"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "expired_cookie"
|
||||
|
||||
crawl_results = [mock_crawl_failed_result, mock_crawl_answer_result]
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', side_effect=crawl_results):
|
||||
with patch.object(adapter, '_trigger_qrcode_login', return_value=mock_zhihu_login_result):
|
||||
content = await adapter.fetch_content("789012", "answer")
|
||||
|
||||
# Verify login was triggered and cookie was updated
|
||||
assert adapter._cookie == mock_zhihu_login_result.cookie_str
|
||||
assert "Test Question Title for Pipeline Integration" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_failure_raises_error(self, mock_crawl_failed_result):
|
||||
"""Test that failed login raises appropriate error"""
|
||||
from kb_workflow.auth.zhihu_login import ZhihuLoginResult
|
||||
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "expired_cookie"
|
||||
|
||||
failed_login = ZhihuLoginResult(
|
||||
success=False,
|
||||
cookie_str="",
|
||||
error_message="Login timeout"
|
||||
)
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_failed_result):
|
||||
with patch.object(adapter, '_trigger_qrcode_login', return_value=failed_login):
|
||||
with pytest.raises(AuthError) as exc_info:
|
||||
await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
assert exc_info.value.auth_type == "zhihu_cookie"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Comment Formatting Tests (Task 3)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestZhihuAdapterCommentFormatting:
|
||||
"""Tests for comment formatting functionality"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_comment_formatting_with_comments(self, mock_crawl_answer_result):
|
||||
"""Test that comments are formatted with proper Markdown structure.
|
||||
|
||||
Verifies:
|
||||
- Comments section starts with "---" separator
|
||||
- "## 热门评论" heading is present
|
||||
- Each comment is numbered with bold author name
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_answer_result):
|
||||
content = await adapter.fetch_content("789012", "answer")
|
||||
|
||||
# Check comment section structure
|
||||
assert "---" in content
|
||||
assert "## 热门评论" in content
|
||||
|
||||
# Check comment formatting: numbered list with author
|
||||
assert "1. **@Commenter1**" in content or "1. **@Commenter1**" in content
|
||||
assert "Great answer!" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_comment_formatting_no_comments(self, mock_crawl_no_comments_result):
|
||||
"""Test that no comments section is added when there are no comments.
|
||||
|
||||
Verifies:
|
||||
- No "---" separator for empty comments
|
||||
- No "## 热门评论" heading when comments list is empty
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_no_comments_result):
|
||||
content = await adapter.fetch_content("789012", "answer")
|
||||
|
||||
# No comments section should be present
|
||||
assert "## 热门评论" not in content
|
||||
|
||||
def test_format_comments_section(self, mock_comments_list):
|
||||
"""Test internal _format_comments_section method.
|
||||
|
||||
Verifies:
|
||||
- Comments are formatted as numbered list
|
||||
- Author name is bold/mentioned
|
||||
- Like count is included
|
||||
- Format matches expected style:
|
||||
1. **@AuthorName** (100 likes): Comment content...
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test"
|
||||
|
||||
result = adapter._format_comments_section(mock_comments_list)
|
||||
|
||||
# Check format structure
|
||||
assert "1. **@User1**" in result
|
||||
assert "(100 likes)" in result or "100 likes" in result
|
||||
assert "This is the first comment" in result
|
||||
|
||||
# Check author comment indicator
|
||||
assert "@AuthorName" in result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Idempotency Tests (Task 3)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestZhihuAdapterIdempotency:
|
||||
"""Tests for idempotent execution"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_idempotent_execution_skips_processed(self, mock_crawl_answer_result, tmp_path):
|
||||
"""Test that idempotent check prevents reprocessing when output_dir is set.
|
||||
|
||||
Verifies:
|
||||
- If output_dir is set and content is already processed, cached result is returned
|
||||
- No duplicate fetches are made
|
||||
"""
|
||||
import json
|
||||
|
||||
adapter = ZhihuAdapter(output_dir=tmp_path)
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
# Create existing files to simulate already processed content
|
||||
metadata_file = tmp_path / "zhihu_789012_metadata.json"
|
||||
content_file = tmp_path / "zhihu_789012_content.md"
|
||||
cached_metadata = {
|
||||
"title": "Cached Title",
|
||||
"author": "Cached Author",
|
||||
"type": "answer"
|
||||
}
|
||||
metadata_file.write_text(json.dumps(cached_metadata))
|
||||
content_file.write_text("Cached content")
|
||||
|
||||
# When content is already processed, should return cached result
|
||||
with patch.object(adapter, '_crawl_with_crawler') as mock_crawl:
|
||||
metadata = await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
# Should NOT have called the crawler since content is cached
|
||||
mock_crawl.assert_not_called()
|
||||
|
||||
# Should return cached metadata
|
||||
assert metadata["title"] == "Cached Title"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_content_processes_independently(self, mock_crawl_answer_result, mock_crawl_article_result):
|
||||
"""Test that different content IDs are processed independently.
|
||||
|
||||
Verifies:
|
||||
- Processing one content doesn't affect processing of another
|
||||
- Each content_id is tracked separately
|
||||
"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test_cookie"
|
||||
|
||||
# Process answer
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_answer_result):
|
||||
metadata_answer = await adapter.fetch_metadata("789012", "answer")
|
||||
|
||||
# Process article
|
||||
with patch.object(adapter, '_crawl_with_crawler', return_value=mock_crawl_article_result):
|
||||
metadata_article = await adapter.fetch_metadata("123456", "zhuanlan")
|
||||
|
||||
# Both should return their respective data
|
||||
assert metadata_answer["title"] == "Test Question Title for Pipeline Integration"
|
||||
assert metadata_article["title"] == "Test Article Title for Pipeline Integration"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cookie Settings Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestZhihuAdapterCookieSettings:
|
||||
"""Tests for cookie loading from settings"""
|
||||
|
||||
def test_cookie_from_settings(self):
|
||||
"""Test cookie is loaded from settings"""
|
||||
@@ -357,99 +610,3 @@ class TestZhihuAdapterCookieHandling:
|
||||
|
||||
adapter = ZhihuAdapter()
|
||||
assert adapter.cookie == "test_cookie_value"
|
||||
|
||||
def test_platform_constant(self):
|
||||
"""Test PLATFORM constant"""
|
||||
adapter = ZhihuAdapter()
|
||||
adapter._cookie = "test" # Avoid config loading
|
||||
assert adapter.PLATFORM == "zhihu"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Wave 0 Test Skeletons for Pipeline Integration (Phase 09)
|
||||
# These tests are marked skip until ZhihuAdapter is updated to use ZhihuCrawler
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Wave 0 skeleton - awaiting ZhihuCrawler integration")
|
||||
class TestZhihuAdapterCrawlerIntegration:
|
||||
"""Test skeletons for ZhihuAdapter integration with ZhihuCrawler.
|
||||
|
||||
These tests will verify the adapter correctly uses the new ZhihuCrawler
|
||||
instead of ZhihuPlaywrightExtractor.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_answer(self, mock_crawl_answer_result):
|
||||
"""Test fetching answer metadata via ZhihuCrawler.
|
||||
|
||||
Verifies:
|
||||
- ZhihuCrawler.crawl_answer() is called with correct ID
|
||||
- Metadata is correctly extracted from CrawlResult
|
||||
- Output format matches expected adapter response
|
||||
"""
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_article(self, mock_crawl_article_result):
|
||||
"""Test fetching zhuanlan article metadata via ZhihuCrawler.
|
||||
|
||||
Verifies:
|
||||
- ZhihuCrawler.crawl_article() is called with correct ID
|
||||
- Metadata is correctly extracted from CrawlResult
|
||||
- Output format matches expected adapter response
|
||||
"""
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_content_with_comments(
|
||||
self, mock_crawl_answer_result, mock_comments_list
|
||||
):
|
||||
"""Test fetching content with comments appended.
|
||||
|
||||
Verifies:
|
||||
- Content is fetched with include_comments=True
|
||||
- Comments are appended to content in correct Markdown format
|
||||
- Comment format: numbered list with author mention
|
||||
"""
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cookie_expired_triggers_login(
|
||||
self, mock_crawl_failed_result, mock_zhihu_login_result
|
||||
):
|
||||
"""Test that expired cookie triggers QR code login flow.
|
||||
|
||||
Verifies:
|
||||
- CrawlResult with success=False triggers login
|
||||
- ZhihuLogin.login_by_qrcode() is called
|
||||
- New cookie is used to retry the fetch
|
||||
- User sees QR code prompt
|
||||
"""
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_idempotent_execution(self, mock_crawl_answer_result):
|
||||
"""Test that repeated calls with same content_id are idempotent.
|
||||
|
||||
Verifies:
|
||||
- Multiple calls with same ID produce same result
|
||||
- No duplicate fetches are made
|
||||
- State is correctly managed
|
||||
"""
|
||||
pass
|
||||
|
||||
def test_comment_formatting(self, mock_comments_list):
|
||||
"""Test comment formatting to Markdown.
|
||||
|
||||
Verifies:
|
||||
- Comments are formatted as numbered list
|
||||
- Author name is bold/mentioned
|
||||
- Like count is included
|
||||
- Format matches expected style:
|
||||
1. **@AuthorName** (100 likes): Comment content...
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# WAVE_0_COMPLETE: Test scaffolding ready for implementation
|
||||
|
||||
Reference in New Issue
Block a user