test_composition_tools_coverage.py•26.6 kB
"""Comprehensive tests for composition_tools.py coverage.
Tests all error handling paths and edge cases.
"""
from unittest.mock import MagicMock, patch
import pytest
from chuk_mcp_linkedin.manager import Draft
from chuk_mcp_linkedin.tools import composition_tools
@pytest.fixture
def mock_mcp():
"""Create a mock MCP server"""
mcp = MagicMock()
mcp.registered_tools = {}
def tool_decorator(func):
mcp.registered_tools[func.__name__] = func
return func
mcp.tool = tool_decorator
return mcp
@pytest.fixture
def mock_manager():
"""Create a mock manager"""
manager = MagicMock()
manager.current_draft_id = "draft-123"
return manager
class TestCompositionToolsErrorPaths:
"""Test error handling paths in composition tools"""
@pytest.fixture(autouse=True)
def patch_manager(self, mock_manager):
"""Automatically patch get_current_manager for all tests in this class"""
with patch(
"chuk_mcp_linkedin.tools.composition_tools.get_current_manager",
return_value=mock_manager,
):
yield
@pytest.mark.asyncio
async def test_add_hook_exception_handling(self, mock_mcp, mock_manager):
"""Test hook with exception from ComposablePost"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
# Trigger validation error by using invalid hook type with ComposablePost
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_hook.side_effect = ValueError("Invalid hook type")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_hook"]("invalid_type", "content")
assert "Invalid hook type" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_body_exception_handling(self, mock_mcp, mock_manager):
"""Test body with exception from ComposablePost"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_body.side_effect = ValueError("Content too long")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_body"]("x" * 10000, "linear")
assert "Content too long" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_cta_exception_handling(self, mock_mcp, mock_manager):
"""Test CTA with exception from ComposablePost"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_cta.side_effect = ValueError("Invalid CTA type")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_cta"]("invalid", "text")
assert "Invalid CTA type" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_bar_chart_exception(self, mock_mcp, mock_manager):
"""Test bar chart with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_bar_chart.side_effect = ValueError("Invalid data")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_bar_chart"]({"A": "invalid"}, "Title")
assert "Invalid data" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_metrics_chart_exception(self, mock_mcp, mock_manager):
"""Test metrics chart with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_metrics_chart.side_effect = ValueError("Invalid metrics")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_metrics_chart"]({"A": 123}, "Title")
assert "Invalid metrics" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_comparison_chart_exception(self, mock_mcp, mock_manager):
"""Test comparison chart with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_comparison_chart.side_effect = ValueError("Need 2 options")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_comparison_chart"]({"A": "only one"}, "Title")
assert "Need 2 options" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_progress_chart_exception(self, mock_mcp, mock_manager):
"""Test progress chart with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_progress_chart.side_effect = ValueError("Invalid percentage")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_progress_chart"]({"A": 150}, "Title")
assert "Invalid percentage" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_ranking_chart_exception(self, mock_mcp, mock_manager):
"""Test ranking chart with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_ranking_chart.side_effect = ValueError("Invalid ranking data")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_ranking_chart"]({}, "Title")
assert "Invalid ranking data" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_quote_exception(self, mock_mcp, mock_manager):
"""Test quote with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_quote.side_effect = ValueError("Quote too long")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_quote"]("x" * 1000, "Author")
assert "Quote too long" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_big_stat_exception(self, mock_mcp, mock_manager):
"""Test big stat with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_big_stat.side_effect = ValueError("Invalid stat")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_big_stat"]("", "label")
assert "Invalid stat" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_timeline_exception(self, mock_mcp, mock_manager):
"""Test timeline with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_timeline.side_effect = ValueError("Empty timeline")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_timeline"]({}, "Title")
assert "Empty timeline" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_key_takeaway_exception(self, mock_mcp, mock_manager):
"""Test key takeaway with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_key_takeaway.side_effect = ValueError("Message too long")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_key_takeaway"]("x" * 1000)
assert "Message too long" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_pro_con_exception(self, mock_mcp, mock_manager):
"""Test pro/con with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_pro_con.side_effect = ValueError("Empty lists")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_pro_con"]([], [])
assert "Empty lists" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_separator_exception(self, mock_mcp, mock_manager):
"""Test separator with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_separator.side_effect = ValueError("Invalid style")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_separator"]("invalid")
assert "Invalid style" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_checklist_exception(self, mock_mcp, mock_manager):
"""Test checklist with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_checklist.side_effect = ValueError("Invalid checklist items")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_checklist"]([])
assert "Invalid checklist items" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_before_after_exception(self, mock_mcp, mock_manager):
"""Test before/after with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_before_after.side_effect = ValueError("Mismatched lists")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_before_after"]([], [])
assert "Mismatched lists" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_tip_box_exception(self, mock_mcp, mock_manager):
"""Test tip box with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_tip_box.side_effect = ValueError("Message empty")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_tip_box"]("")
assert "Message empty" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_stats_grid_exception(self, mock_mcp, mock_manager):
"""Test stats grid with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_stats_grid.side_effect = ValueError("Invalid columns")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_stats_grid"]({"A": "1"}, columns=10)
assert "Invalid columns" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_poll_preview_exception(self, mock_mcp, mock_manager):
"""Test poll preview with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_poll_preview.side_effect = ValueError("Not enough options")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_poll_preview"]("Question?", ["A"])
assert "Not enough options" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_feature_list_exception(self, mock_mcp, mock_manager):
"""Test feature list with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_feature_list.side_effect = ValueError("Missing title")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_feature_list"]([{"no_title": "oops"}])
assert "Missing title" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_numbered_list_exception(self, mock_mcp, mock_manager):
"""Test numbered list with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_numbered_list.side_effect = ValueError("Empty list")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_numbered_list"]([])
assert "Empty list" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_add_hashtags_exception(self, mock_mcp, mock_manager):
"""Test hashtags with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.add_hashtags.side_effect = ValueError("No tags")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_add_hashtags"]([])
assert "No tags" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_compose_post_exception(self, mock_mcp, mock_manager):
"""Test compose post with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.compose.side_effect = ValueError("Content exceeds limit")
mock_post.optimize_for_engagement.return_value = None
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_compose_post"](optimize=True)
assert "Content exceeds limit" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_get_preview_exception(self, mock_mcp, mock_manager):
"""Test get preview with exception"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
with patch("chuk_mcp_linkedin.tools.composition_tools.ComposablePost") as _:
mock_post = MagicMock()
mock_post.get_preview.side_effect = ValueError("No content")
# Add mock to cache
composition_tools._post_cache["draft-123"] = mock_post
result = await tools["linkedin_get_preview"]()
assert "No content" in result
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_get_or_create_post_with_theme(self, mock_mcp, mock_manager):
"""Test _get_or_create_post creates post with theme"""
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme="thought_leader"
)
mock_manager.get_current_draft.return_value = mock_draft
tools = register_composition_tools(mock_mcp)
# Call any tool to trigger _get_or_create_post with theme
result = await tools["linkedin_add_body"]("Test content")
assert "Added body" in result
# Verify ComposablePost was created in cache with theme
assert "draft-123" in composition_tools._post_cache
# Cleanup cache
composition_tools._post_cache.clear()
@pytest.mark.asyncio
async def test_preview_html_failed_generation(self, mock_mcp, mock_manager):
"""Test HTML preview when generation fails"""
from unittest.mock import AsyncMock
from chuk_mcp_linkedin.tools.composition_tools import register_composition_tools
mock_draft = Draft(
draft_id="draft-123", name="Test", post_type="text", content={}, theme=None
)
mock_manager.get_current_draft.return_value = mock_draft
mock_manager.generate_preview_url = AsyncMock(return_value=None) # Failure
tools = register_composition_tools(mock_mcp)
result = await tools["linkedin_preview_html"](open_browser=False)
assert "Failed to generate preview" in result