995 lines
36 KiB
Python
995 lines
36 KiB
Python
"""Tests for the content creation pipeline tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from cheddahbot.config import Config, ContentConfig
|
|
from cheddahbot.tools.content_creation import (
|
|
_build_optimization_prompt,
|
|
_build_phase1_prompt,
|
|
_build_phase2_prompt,
|
|
_finalize_optimization,
|
|
_find_cora_report,
|
|
_run_optimization,
|
|
_save_content,
|
|
_slugify,
|
|
_sync_clickup_optimization_complete,
|
|
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_for_new_content(self, tmp_db, tmp_path):
|
|
ctx = self._make_ctx(tmp_db, tmp_path)
|
|
result = create_content(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_optimization_prompt
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBuildOptimizationPrompt:
|
|
def test_contains_url_and_keyword(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com/plumbing",
|
|
keyword="plumbing services",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
)
|
|
assert "https://example.com/plumbing" in prompt
|
|
assert "plumbing services" in prompt
|
|
|
|
def test_contains_cora_path(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com",
|
|
keyword="kw",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
)
|
|
assert "Z:/cora/report.xlsx" in prompt
|
|
|
|
def test_contains_all_script_commands(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com",
|
|
keyword="kw",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
)
|
|
assert "competitor_scraper.py" in prompt
|
|
assert "test_block_prep.py" in prompt
|
|
assert "test_block_generator.py" in prompt
|
|
assert "test_block_validate.py" in prompt
|
|
|
|
def test_contains_step8_instructions(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com",
|
|
keyword="kw",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
)
|
|
assert "optimization_instructions.md" in prompt
|
|
assert "Heading Changes" in prompt
|
|
assert "Entity Integration Points" in prompt
|
|
assert "Meta Tag Updates" in prompt
|
|
assert "Priority Ranking" in prompt
|
|
|
|
def test_service_page_note(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com",
|
|
keyword="kw",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
is_service_page=True,
|
|
capabilities_default="Check website.",
|
|
)
|
|
assert "service page" in prompt
|
|
assert "Check website." in prompt
|
|
|
|
def test_no_service_page_note_by_default(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com",
|
|
keyword="kw",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
)
|
|
assert "service page" not in prompt.lower().split("step")[0]
|
|
|
|
def test_all_eight_steps_present(self):
|
|
prompt = _build_optimization_prompt(
|
|
url="https://example.com",
|
|
keyword="kw",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
work_dir="/tmp/work",
|
|
scripts_dir="/scripts",
|
|
)
|
|
for step_num in range(1, 9):
|
|
assert f"Step {step_num}" in prompt
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _run_optimization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRunOptimization:
|
|
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 = "Optimization complete"
|
|
return {
|
|
"agent": agent,
|
|
"config": cfg,
|
|
"db": tmp_db,
|
|
"clickup_task_id": "opt_task_1",
|
|
}
|
|
|
|
def test_fails_without_cora_report(self, tmp_db, tmp_path):
|
|
ctx = self._make_ctx(tmp_db, tmp_path)
|
|
result = _run_optimization(
|
|
agent=ctx["agent"],
|
|
config=ctx["config"],
|
|
ctx=ctx,
|
|
task_id="opt_task_1",
|
|
url="https://example.com",
|
|
keyword="plumbing services",
|
|
cora_path="",
|
|
)
|
|
assert "Error:" in result
|
|
assert "Cora report" in result
|
|
|
|
@patch("cheddahbot.tools.content_creation._sync_clickup_fail")
|
|
def test_syncs_clickup_on_missing_cora(self, mock_fail, tmp_db, tmp_path):
|
|
ctx = self._make_ctx(tmp_db, tmp_path)
|
|
_run_optimization(
|
|
agent=ctx["agent"],
|
|
config=ctx["config"],
|
|
ctx=ctx,
|
|
task_id="opt_task_1",
|
|
url="https://example.com",
|
|
keyword="plumbing services",
|
|
cora_path="",
|
|
)
|
|
mock_fail.assert_called_once()
|
|
assert mock_fail.call_args[0][1] == "opt_task_1"
|
|
|
|
@patch("cheddahbot.tools.content_creation._finalize_optimization")
|
|
@patch("cheddahbot.tools.content_creation._sync_clickup_start")
|
|
def test_creates_work_dir_and_calls_execute(
|
|
self, mock_start, mock_finalize, tmp_db, tmp_path
|
|
):
|
|
ctx = self._make_ctx(tmp_db, tmp_path)
|
|
mock_finalize.return_value = "finalized"
|
|
with patch(
|
|
"cheddahbot.tools.content_creation._LOCAL_CONTENT_DIR",
|
|
tmp_path / "content",
|
|
):
|
|
result = _run_optimization(
|
|
agent=ctx["agent"],
|
|
config=ctx["config"],
|
|
ctx=ctx,
|
|
task_id="opt_task_1",
|
|
url="https://example.com/plumbing",
|
|
keyword="plumbing services",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
)
|
|
ctx["agent"].execute_task.assert_called_once()
|
|
mock_start.assert_called_once_with(ctx, "opt_task_1")
|
|
mock_finalize.assert_called_once()
|
|
assert result == "finalized"
|
|
|
|
@patch("cheddahbot.tools.content_creation._sync_clickup_fail")
|
|
@patch("cheddahbot.tools.content_creation._sync_clickup_start")
|
|
def test_syncs_clickup_on_execution_error(
|
|
self, mock_start, mock_fail, tmp_db, tmp_path
|
|
):
|
|
ctx = self._make_ctx(tmp_db, tmp_path)
|
|
ctx["agent"].execute_task.side_effect = RuntimeError("CLI crashed")
|
|
with patch(
|
|
"cheddahbot.tools.content_creation._LOCAL_CONTENT_DIR",
|
|
tmp_path / "content",
|
|
):
|
|
result = _run_optimization(
|
|
agent=ctx["agent"],
|
|
config=ctx["config"],
|
|
ctx=ctx,
|
|
task_id="opt_task_1",
|
|
url="https://example.com",
|
|
keyword="plumbing services",
|
|
cora_path="Z:/cora/report.xlsx",
|
|
)
|
|
assert "Error:" in result
|
|
mock_fail.assert_called_once()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _finalize_optimization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFinalizeOptimization:
|
|
def _make_config(self, outline_dir: str = "") -> Config:
|
|
cfg = Config()
|
|
cfg.content = ContentConfig(outline_dir=outline_dir)
|
|
return cfg
|
|
|
|
def test_errors_on_missing_test_block(self, tmp_path):
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
# Only create instructions, not test_block.html
|
|
(work_dir / "optimization_instructions.md").write_text("instructions")
|
|
cfg = self._make_config()
|
|
result = _finalize_optimization(
|
|
ctx=None,
|
|
config=cfg,
|
|
task_id="",
|
|
keyword="kw",
|
|
url="https://example.com",
|
|
work_dir=work_dir,
|
|
exec_result="done",
|
|
)
|
|
assert "Error:" in result
|
|
assert "test_block.html" in result
|
|
|
|
def test_errors_on_missing_instructions(self, tmp_path):
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
# Only create test_block, not instructions
|
|
(work_dir / "test_block.html").write_text("<div>block</div>")
|
|
cfg = self._make_config()
|
|
result = _finalize_optimization(
|
|
ctx=None,
|
|
config=cfg,
|
|
task_id="",
|
|
keyword="kw",
|
|
url="https://example.com",
|
|
work_dir=work_dir,
|
|
exec_result="done",
|
|
)
|
|
assert "Error:" in result
|
|
assert "optimization_instructions.md" in result
|
|
|
|
def test_succeeds_with_required_files(self, tmp_path):
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
(work_dir / "test_block.html").write_text("<div>block</div>")
|
|
(work_dir / "optimization_instructions.md").write_text("# Instructions")
|
|
cfg = self._make_config()
|
|
result = _finalize_optimization(
|
|
ctx=None,
|
|
config=cfg,
|
|
task_id="",
|
|
keyword="plumbing services",
|
|
url="https://example.com",
|
|
work_dir=work_dir,
|
|
exec_result="all done",
|
|
)
|
|
assert "Optimization Complete" in result
|
|
assert "plumbing services" in result
|
|
assert "test_block.html" in result
|
|
|
|
def test_copies_to_network_path(self, tmp_path):
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
(work_dir / "test_block.html").write_text("<div>block</div>")
|
|
(work_dir / "optimization_instructions.md").write_text("# Instructions")
|
|
net_dir = tmp_path / "network"
|
|
cfg = self._make_config(str(net_dir))
|
|
_finalize_optimization(
|
|
ctx=None,
|
|
config=cfg,
|
|
task_id="",
|
|
keyword="plumbing services",
|
|
url="https://example.com",
|
|
work_dir=work_dir,
|
|
exec_result="done",
|
|
)
|
|
assert (net_dir / "plumbing-services" / "test_block.html").exists()
|
|
assert (net_dir / "plumbing-services" / "optimization_instructions.md").exists()
|
|
|
|
@patch("cheddahbot.tools.content_creation._sync_clickup_optimization_complete")
|
|
def test_syncs_clickup_when_task_id_present(self, mock_sync, tmp_path, tmp_db):
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
(work_dir / "test_block.html").write_text("<div>block</div>")
|
|
(work_dir / "optimization_instructions.md").write_text("# Instructions")
|
|
cfg = self._make_config()
|
|
ctx = {"config": cfg, "db": tmp_db}
|
|
_finalize_optimization(
|
|
ctx=ctx,
|
|
config=cfg,
|
|
task_id="task_fin",
|
|
keyword="kw",
|
|
url="https://example.com",
|
|
work_dir=work_dir,
|
|
exec_result="done",
|
|
)
|
|
mock_sync.assert_called_once()
|
|
call_kwargs = mock_sync.call_args.kwargs
|
|
assert call_kwargs["task_id"] == "task_fin"
|
|
assert "test_block.html" in call_kwargs["found_files"]
|
|
assert "optimization_instructions.md" in call_kwargs["found_files"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _sync_clickup_optimization_complete
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSyncClickupOptimizationComplete:
|
|
@patch("cheddahbot.tools.content_creation._get_clickup_client")
|
|
def test_uploads_files_and_posts_comment(self, mock_get_client, tmp_path):
|
|
mock_client = MagicMock()
|
|
mock_get_client.return_value = mock_client
|
|
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
tb_path = work_dir / "test_block.html"
|
|
tb_path.write_text("<div>block</div>")
|
|
inst_path = work_dir / "optimization_instructions.md"
|
|
inst_path.write_text("# Instructions")
|
|
val_path = work_dir / "validation_report.json"
|
|
val_path.write_text(json.dumps({"summary": "All metrics improved."}))
|
|
|
|
cfg = Config()
|
|
ctx = {"config": cfg}
|
|
found_files = {
|
|
"test_block.html": tb_path,
|
|
"optimization_instructions.md": inst_path,
|
|
"validation_report.json": val_path,
|
|
}
|
|
_sync_clickup_optimization_complete(
|
|
ctx=ctx,
|
|
config=cfg,
|
|
task_id="task_sync",
|
|
keyword="plumbing",
|
|
url="https://example.com",
|
|
found_files=found_files,
|
|
work_dir=work_dir,
|
|
)
|
|
# 3 file uploads
|
|
assert mock_client.upload_attachment.call_count == 3
|
|
# Comment posted
|
|
mock_client.add_comment.assert_called_once()
|
|
comment = mock_client.add_comment.call_args[0][1]
|
|
assert "plumbing" in comment
|
|
assert "All metrics improved." in comment
|
|
assert "Next Steps" in comment
|
|
# Status set to internal review
|
|
mock_client.update_task_status.assert_called_once_with(
|
|
"task_sync", cfg.clickup.review_status
|
|
)
|
|
|
|
@patch("cheddahbot.tools.content_creation._get_clickup_client")
|
|
def test_handles_no_validation_report(self, mock_get_client, tmp_path):
|
|
mock_client = MagicMock()
|
|
mock_get_client.return_value = mock_client
|
|
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
tb_path = work_dir / "test_block.html"
|
|
tb_path.write_text("<div>block</div>")
|
|
inst_path = work_dir / "optimization_instructions.md"
|
|
inst_path.write_text("# Instructions")
|
|
|
|
cfg = Config()
|
|
ctx = {"config": cfg}
|
|
found_files = {
|
|
"test_block.html": tb_path,
|
|
"optimization_instructions.md": inst_path,
|
|
}
|
|
_sync_clickup_optimization_complete(
|
|
ctx=ctx,
|
|
config=cfg,
|
|
task_id="task_sync2",
|
|
keyword="kw",
|
|
url="https://example.com",
|
|
found_files=found_files,
|
|
work_dir=work_dir,
|
|
)
|
|
# 2 uploads (no validation_report.json)
|
|
assert mock_client.upload_attachment.call_count == 2
|
|
mock_client.add_comment.assert_called_once()
|
|
|
|
def test_noop_without_task_id(self, tmp_path):
|
|
"""No ClickUp sync when task_id is empty."""
|
|
work_dir = tmp_path / "work"
|
|
work_dir.mkdir()
|
|
cfg = Config()
|
|
# Should not raise
|
|
_sync_clickup_optimization_complete(
|
|
ctx={"config": cfg},
|
|
config=cfg,
|
|
task_id="",
|
|
keyword="kw",
|
|
url="https://example.com",
|
|
found_files={},
|
|
work_dir=work_dir,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create_content — Routing (URL → optimization vs new content → phases)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCreateContentRouting:
|
|
@patch("cheddahbot.tools.content_creation._run_optimization")
|
|
def test_explicit_optimization_routes_correctly(self, mock_opt, tmp_db, tmp_path):
|
|
"""When content_type='on page optimization', routes to _run_optimization."""
|
|
mock_opt.return_value = "## Optimization Complete"
|
|
cfg = Config()
|
|
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
|
|
ctx = {
|
|
"agent": MagicMock(),
|
|
"config": cfg,
|
|
"db": tmp_db,
|
|
"clickup_task_id": "routing_test",
|
|
}
|
|
result = create_content(
|
|
keyword="plumbing services",
|
|
url="https://example.com/plumbing",
|
|
content_type="on page optimization",
|
|
ctx=ctx,
|
|
)
|
|
mock_opt.assert_called_once()
|
|
assert result == "## Optimization Complete"
|
|
|
|
@patch("cheddahbot.tools.content_creation._run_optimization")
|
|
def test_explicit_new_content_with_url_routes_to_phase1(self, mock_opt, tmp_db, tmp_path):
|
|
"""Content Creation with URL should go to Phase 1, NOT optimization."""
|
|
cfg = Config()
|
|
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
|
|
agent = MagicMock()
|
|
agent.execute_task.return_value = "## Outline"
|
|
ctx = {
|
|
"agent": agent,
|
|
"config": cfg,
|
|
"db": tmp_db,
|
|
"clickup_task_id": "",
|
|
}
|
|
result = create_content(
|
|
keyword="new keyword",
|
|
url="https://example.com/future-page",
|
|
content_type="new content",
|
|
ctx=ctx,
|
|
)
|
|
mock_opt.assert_not_called()
|
|
assert "Phase 1 Complete" in result
|
|
|
|
@patch("cheddahbot.tools.content_creation._run_optimization")
|
|
def test_optimization_without_url_returns_error(self, mock_opt, tmp_db, tmp_path):
|
|
"""On Page Optimization without URL should return an error."""
|
|
cfg = Config()
|
|
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
|
|
ctx = {
|
|
"agent": MagicMock(),
|
|
"config": cfg,
|
|
"db": tmp_db,
|
|
"clickup_task_id": "",
|
|
}
|
|
result = create_content(
|
|
keyword="plumbing services",
|
|
url="",
|
|
content_type="on page optimization",
|
|
ctx=ctx,
|
|
)
|
|
mock_opt.assert_not_called()
|
|
assert "Error" in result
|
|
assert "URL" in result
|
|
|
|
@patch("cheddahbot.tools.content_creation._run_optimization")
|
|
def test_fallback_url_routes_to_optimization(self, mock_opt, tmp_db, tmp_path):
|
|
"""When content_type is empty and URL present, falls back to optimization."""
|
|
mock_opt.return_value = "## Optimization Complete"
|
|
cfg = Config()
|
|
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
|
|
ctx = {
|
|
"agent": MagicMock(),
|
|
"config": cfg,
|
|
"db": tmp_db,
|
|
"clickup_task_id": "routing_test",
|
|
}
|
|
result = create_content(
|
|
keyword="plumbing services",
|
|
url="https://example.com/plumbing",
|
|
content_type="",
|
|
ctx=ctx,
|
|
)
|
|
mock_opt.assert_called_once()
|
|
assert result == "## Optimization Complete"
|
|
|
|
@patch("cheddahbot.tools.content_creation._run_optimization")
|
|
def test_new_content_still_calls_phase1(self, mock_opt, tmp_db, tmp_path):
|
|
"""Regression: new content (no URL, no content_type) still goes through _run_phase1."""
|
|
cfg = Config()
|
|
cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines"))
|
|
agent = MagicMock()
|
|
agent.execute_task.return_value = "## Generated Outline\nContent..."
|
|
ctx = {
|
|
"agent": agent,
|
|
"config": cfg,
|
|
"db": tmp_db,
|
|
"clickup_task_id": "",
|
|
}
|
|
create_content(
|
|
keyword="new topic",
|
|
url="",
|
|
ctx=ctx,
|
|
)
|
|
mock_opt.assert_not_called()
|
|
agent.execute_task.assert_called_once()
|
|
# Verify it's the phase 1 prompt (new content path)
|
|
call_args = agent.execute_task.call_args
|
|
prompt = call_args.args[0] if call_args.args else call_args.kwargs.get("prompt", "")
|
|
assert "new content creation project" in prompt
|