"""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("
block
") 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("
block
") (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("
block
") (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("
block
") (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("
block
") 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("
block
") 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