CheddahBot/tests/test_content_creation.py

493 lines
18 KiB
Python

"""Tests for the content creation pipeline tool."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
from cheddahbot.config import Config, ContentConfig
from cheddahbot.tools.content_creation import (
_build_phase1_prompt,
_build_phase2_prompt,
_find_cora_report,
_save_content,
_slugify,
continue_content,
create_content,
)
# ---------------------------------------------------------------------------
# _slugify
# ---------------------------------------------------------------------------
def test_slugify_basic():
assert _slugify("Plumbing Services") == "plumbing-services"
def test_slugify_special_chars():
assert _slugify("AC Repair & Maintenance!") == "ac-repair-maintenance"
def test_slugify_truncates():
long = "a" * 200
assert len(_slugify(long)) <= 80
# ---------------------------------------------------------------------------
# _build_phase1_prompt
# ---------------------------------------------------------------------------
class TestBuildPhase1Prompt:
def test_contains_trigger_keywords(self):
prompt = _build_phase1_prompt(
"https://example.com/plumbing",
"plumbing services",
"service page",
"",
"",
)
assert "on-page optimization" in prompt
assert "plumbing services" in prompt
assert "https://example.com/plumbing" in prompt
def test_includes_cora_path(self):
prompt = _build_phase1_prompt(
"https://example.com",
"keyword",
"blog post",
"Z:/cora/report.xlsx",
"",
)
assert "Z:/cora/report.xlsx" in prompt
assert "Cora SEO report" in prompt
def test_includes_capabilities_default(self):
default = "Verify on website."
prompt = _build_phase1_prompt(
"https://example.com",
"keyword",
"service page",
"",
default,
)
assert default in prompt
assert "company capabilities" in prompt
def test_no_cora_no_capabilities(self):
prompt = _build_phase1_prompt(
"https://example.com",
"keyword",
"service page",
"",
"",
)
assert "Cora SEO report" not in prompt
assert "company capabilities" not in prompt
# ---------------------------------------------------------------------------
# _build_phase2_prompt
# ---------------------------------------------------------------------------
class TestBuildPhase2Prompt:
def test_contains_outline(self):
outline = "## Section 1\nContent here."
prompt = _build_phase2_prompt(
"https://example.com",
"plumbing",
outline,
"",
)
assert outline in prompt
assert "writing phase" in prompt
assert "plumbing" in prompt
def test_includes_cora_path(self):
prompt = _build_phase2_prompt(
"https://example.com",
"keyword",
"outline text",
"Z:/cora/report.xlsx",
)
assert "Z:/cora/report.xlsx" in prompt
def test_no_cora(self):
prompt = _build_phase2_prompt(
"https://example.com",
"keyword",
"outline text",
"",
)
assert "Cora SEO report" not in prompt
# ---------------------------------------------------------------------------
# _find_cora_report
# ---------------------------------------------------------------------------
class TestFindCoraReport:
def test_empty_inbox(self, tmp_path):
assert _find_cora_report("keyword", str(tmp_path)) == ""
def test_nonexistent_path(self):
assert _find_cora_report("keyword", "/nonexistent/path") == ""
def test_empty_keyword(self, tmp_path):
assert _find_cora_report("", str(tmp_path)) == ""
def test_exact_match(self, tmp_path):
report = tmp_path / "plumbing services.xlsx"
report.touch()
result = _find_cora_report("plumbing services", str(tmp_path))
assert result == str(report)
def test_substring_match(self, tmp_path):
report = tmp_path / "plumbing-services-city.xlsx"
report.touch()
result = _find_cora_report("plumbing services", str(tmp_path))
# "plumbing services" is a substring of "plumbing-services-city"
assert result == str(report)
def test_word_overlap(self, tmp_path):
report = tmp_path / "residential-plumbing-repair.xlsx"
report.touch()
result = _find_cora_report("plumbing repair", str(tmp_path))
assert result == str(report)
def test_skips_temp_files(self, tmp_path):
(tmp_path / "~$report.xlsx").touch()
(tmp_path / "actual-report.xlsx").touch()
result = _find_cora_report("actual report", str(tmp_path))
assert "~$" not in result
assert "actual-report" in result
def test_no_match(self, tmp_path):
(tmp_path / "completely-unrelated.xlsx").touch()
result = _find_cora_report("plumbing services", str(tmp_path))
assert result == ""
# ---------------------------------------------------------------------------
# _save_content
# ---------------------------------------------------------------------------
class TestSaveContent:
def _make_config(self, outline_dir: str = "") -> Config:
cfg = Config()
cfg.content = ContentConfig(outline_dir=outline_dir)
return cfg
def test_saves_to_primary_path(self, tmp_path):
cfg = self._make_config(str(tmp_path / "outlines"))
path = _save_content("# Outline", "plumbing services", "outline.md", cfg)
assert "outlines" in path
assert Path(path).read_text(encoding="utf-8") == "# Outline"
def test_falls_back_to_local(self, tmp_path):
# Point to an invalid network path
cfg = self._make_config("\\\\nonexistent\\share\\outlines")
with patch(
"cheddahbot.tools.content_creation._LOCAL_CONTENT_DIR",
tmp_path / "local",
):
path = _save_content("# Outline", "plumbing", "outline.md", cfg)
assert str(tmp_path / "local") in path
assert Path(path).read_text(encoding="utf-8") == "# Outline"
def test_empty_outline_dir_uses_local(self, tmp_path):
cfg = self._make_config("")
with patch(
"cheddahbot.tools.content_creation._LOCAL_CONTENT_DIR",
tmp_path / "local",
):
path = _save_content("content", "keyword", "outline.md", cfg)
assert str(tmp_path / "local") in path
# ---------------------------------------------------------------------------
# create_content — Phase 1
# ---------------------------------------------------------------------------
class TestCreateContentPhase1:
def _make_ctx(self, tmp_db, tmp_path):
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
agent = MagicMock()
agent.execute_task.return_value = "## Generated Outline\nSection 1..."
return {
"agent": agent,
"config": cfg,
"db": tmp_db,
"clickup_task_id": "task123",
}
def test_requires_keyword(self, tmp_db):
ctx = {"agent": MagicMock(), "config": Config(), "db": tmp_db}
assert create_content(keyword="", ctx=ctx).startswith("Error:")
def test_requires_context(self):
assert create_content(keyword="kw", url="http://x", ctx=None).startswith("Error:")
def test_phase1_runs_without_prior_state(self, tmp_db, tmp_path):
ctx = self._make_ctx(tmp_db, tmp_path)
result = create_content(
url="https://example.com/services",
keyword="plumbing services",
ctx=ctx,
)
assert "Phase 1 Complete" in result
assert "outline" in result.lower()
ctx["agent"].execute_task.assert_called_once()
call_kwargs = ctx["agent"].execute_task.call_args
assert call_kwargs.kwargs.get("skip_permissions") is True
def test_phase1_saves_outline_file(self, tmp_db, tmp_path):
ctx = self._make_ctx(tmp_db, tmp_path)
create_content(
url="https://example.com",
keyword="plumbing services",
ctx=ctx,
)
# The outline should have been saved
outline_dir = tmp_path / "outlines" / "plumbing-services"
assert outline_dir.exists()
saved = (outline_dir / "outline.md").read_text(encoding="utf-8")
assert saved == "## Generated Outline\nSection 1..."
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase1_syncs_clickup(self, mock_get_client, tmp_db, tmp_path):
mock_client = MagicMock()
mock_get_client.return_value = mock_client
ctx = self._make_ctx(tmp_db, tmp_path)
create_content(
url="https://example.com",
keyword="plumbing services",
ctx=ctx,
)
# Verify outline review status was set and OutlinePath was stored
mock_client.update_task_status.assert_any_call("task123", "outline review")
mock_client.set_custom_field_by_name.assert_called_once()
call_args = mock_client.set_custom_field_by_name.call_args
assert call_args[0][0] == "task123"
assert call_args[0][1] == "OutlinePath"
def test_phase1_includes_clickup_sync_marker(self, tmp_db, tmp_path):
ctx = self._make_ctx(tmp_db, tmp_path)
result = create_content(
url="https://example.com",
keyword="test keyword",
ctx=ctx,
)
assert "## ClickUp Sync" in result
# ---------------------------------------------------------------------------
# create_content — Phase 2
# ---------------------------------------------------------------------------
class TestCreateContentPhase2:
def _setup_phase2(self, tmp_db, tmp_path):
"""Set up outline file and return (ctx, outline_path)."""
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
# Create the outline file
outline_dir = tmp_path / "outlines" / "plumbing-services"
outline_dir.mkdir(parents=True)
outline_file = outline_dir / "outline.md"
outline_file.write_text("## Approved Outline\nSection content here.", encoding="utf-8")
agent = MagicMock()
agent.execute_task.return_value = "# Full Content\nParagraph..."
ctx = {
"agent": agent,
"config": cfg,
"db": tmp_db,
"clickup_task_id": "task456",
}
return ctx, str(outline_file)
def _make_phase2_client(self, outline_path):
"""Create a mock ClickUp client that triggers Phase 2 detection."""
mock_client = MagicMock()
mock_task = MagicMock()
mock_task.status = "outline approved"
mock_client.get_task.return_value = mock_task
mock_client.get_custom_field_by_name.return_value = outline_path
return mock_client
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase2_detects_outline_approved_status(self, mock_get_client, tmp_db, tmp_path):
ctx, outline_path = self._setup_phase2(tmp_db, tmp_path)
mock_get_client.return_value = self._make_phase2_client(outline_path)
result = create_content(
url="https://example.com/plumbing",
keyword="plumbing services",
ctx=ctx,
)
assert "Phase 2 Complete" in result
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase2_reads_outline(self, mock_get_client, tmp_db, tmp_path):
ctx, outline_path = self._setup_phase2(tmp_db, tmp_path)
mock_get_client.return_value = self._make_phase2_client(outline_path)
create_content(
url="https://example.com/plumbing",
keyword="plumbing services",
ctx=ctx,
)
call_args = ctx["agent"].execute_task.call_args
prompt = call_args.args[0] if call_args.args else call_args.kwargs.get("prompt", "")
assert "Approved Outline" in prompt
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase2_saves_content_file(self, mock_get_client, tmp_db, tmp_path):
ctx, outline_path = self._setup_phase2(tmp_db, tmp_path)
mock_get_client.return_value = self._make_phase2_client(outline_path)
create_content(
url="https://example.com/plumbing",
keyword="plumbing services",
ctx=ctx,
)
content_file = tmp_path / "outlines" / "plumbing-services" / "final-content.md"
assert content_file.exists()
assert content_file.read_text(encoding="utf-8") == "# Full Content\nParagraph..."
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase2_syncs_clickup_complete(self, mock_get_client, tmp_db, tmp_path):
ctx, outline_path = self._setup_phase2(tmp_db, tmp_path)
mock_client = self._make_phase2_client(outline_path)
mock_get_client.return_value = mock_client
create_content(
url="https://example.com/plumbing",
keyword="plumbing services",
ctx=ctx,
)
# Verify ClickUp was synced to internal review
mock_client.update_task_status.assert_any_call("task456", "internal review")
mock_client.add_comment.assert_called()
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase2_includes_clickup_sync_marker(self, mock_get_client, tmp_db, tmp_path):
ctx, outline_path = self._setup_phase2(tmp_db, tmp_path)
mock_get_client.return_value = self._make_phase2_client(outline_path)
result = create_content(
url="https://example.com/plumbing",
keyword="plumbing services",
ctx=ctx,
)
assert "## ClickUp Sync" in result
# ---------------------------------------------------------------------------
# continue_content
# ---------------------------------------------------------------------------
class TestContinueContent:
def test_requires_keyword(self, tmp_db):
ctx = {"agent": MagicMock(), "db": tmp_db, "config": Config()}
assert continue_content(keyword="", ctx=ctx).startswith("Error:")
def test_no_matching_entry(self, tmp_db):
ctx = {"agent": MagicMock(), "db": tmp_db, "config": Config()}
result = continue_content(keyword="nonexistent", ctx=ctx)
assert "No outline awaiting review" in result
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_finds_and_runs_phase2(self, mock_get_client, tmp_db, tmp_path):
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
cfg.clickup.space_id = "sp1"
# Create outline file
outline_dir = tmp_path / "outlines" / "plumbing-services"
outline_dir.mkdir(parents=True)
outline_file = outline_dir / "outline.md"
outline_file.write_text("## Outline", encoding="utf-8")
# Mock ClickUp client — returns a task matching the keyword
mock_client = MagicMock()
mock_task = MagicMock()
mock_task.id = "task789"
mock_task.custom_fields = {
"Keyword": "plumbing services",
"IMSURL": "https://example.com",
}
mock_client.get_tasks_from_space.return_value = [mock_task]
mock_client.get_custom_field_by_name.return_value = str(outline_file)
mock_get_client.return_value = mock_client
agent = MagicMock()
agent.execute_task.return_value = "# Full content"
ctx = {"agent": agent, "db": tmp_db, "config": cfg}
result = continue_content(keyword="plumbing services", ctx=ctx)
assert "Phase 2 Complete" in result
# ---------------------------------------------------------------------------
# Error propagation
# ---------------------------------------------------------------------------
class TestErrorPropagation:
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase1_execution_error_syncs_clickup(self, mock_get_client, tmp_db, tmp_path):
mock_client = MagicMock()
mock_get_client.return_value = mock_client
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
agent = MagicMock()
agent.execute_task.side_effect = RuntimeError("CLI crashed")
ctx = {
"agent": agent,
"config": cfg,
"db": tmp_db,
"clickup_task_id": "task_err",
}
result = create_content(
url="https://example.com",
keyword="test",
ctx=ctx,
)
assert "Error:" in result
# Verify ClickUp was notified of the failure
mock_client.update_task_status.assert_any_call("task_err", "error")
@patch("cheddahbot.tools.content_creation._get_clickup_client")
def test_phase1_error_return_syncs_clickup(self, mock_get_client, tmp_db, tmp_path):
mock_client = MagicMock()
mock_get_client.return_value = mock_client
cfg = Config()
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
agent = MagicMock()
agent.execute_task.return_value = "Error: something went wrong"
ctx = {
"agent": agent,
"config": cfg,
"db": tmp_db,
"clickup_task_id": "task_err2",
}
result = create_content(
url="https://example.com",
keyword="test",
ctx=ctx,
)
assert result.startswith("Error:")
# Verify ClickUp was notified of the failure
mock_client.update_task_status.assert_any_call("task_err2", "error")