"""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_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..." def test_phase1_sets_kv_state(self, tmp_db, tmp_path): ctx = self._make_ctx(tmp_db, tmp_path) create_content( url="https://example.com", keyword="plumbing services", ctx=ctx, ) raw = tmp_db.kv_get("clickup:task:task123:state") assert raw is not None state = json.loads(raw) assert state["state"] == "outline_review" assert state["keyword"] == "plumbing services" assert state["url"] == "https://example.com" assert "outline_path" in state 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 an outline_review state and outline file, return ctx.""" 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") # Set kv_store to outline_review state = { "state": "outline_review", "clickup_task_id": "task456", "url": "https://example.com/plumbing", "keyword": "plumbing services", "content_type": "service page", "cora_path": "", "outline_path": str(outline_file), } tmp_db.kv_set("clickup:task:task456:state", json.dumps(state)) agent = MagicMock() agent.execute_task.return_value = "# Full Content\nParagraph..." return { "agent": agent, "config": cfg, "db": tmp_db, "clickup_task_id": "task456", } def test_phase2_detects_outline_review_state(self, tmp_db, tmp_path): ctx = self._setup_phase2(tmp_db, tmp_path) result = create_content( url="https://example.com/plumbing", keyword="plumbing services", ctx=ctx, ) assert "Phase 2 Complete" in result def test_phase2_reads_outline(self, tmp_db, tmp_path): ctx = self._setup_phase2(tmp_db, tmp_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 def test_phase2_saves_content_file(self, tmp_db, tmp_path): ctx = self._setup_phase2(tmp_db, tmp_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..." def test_phase2_sets_completed_state(self, tmp_db, tmp_path): ctx = self._setup_phase2(tmp_db, tmp_path) create_content( url="https://example.com/plumbing", keyword="plumbing services", ctx=ctx, ) raw = tmp_db.kv_get("clickup:task:task456:state") state = json.loads(raw) assert state["state"] == "completed" assert "content_path" in state def test_phase2_includes_clickup_sync_marker(self, tmp_db, tmp_path): ctx = self._setup_phase2(tmp_db, tmp_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 def test_finds_and_runs_phase2(self, tmp_db, tmp_path): cfg = Config() cfg.content = ContentConfig(outline_dir=str(tmp_path / "outlines")) # 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") # Set kv state state = { "state": "outline_review", "clickup_task_id": "task789", "url": "https://example.com", "keyword": "plumbing services", "outline_path": str(outline_file), "cora_path": "", } tmp_db.kv_set("clickup:task:task789:state", json.dumps(state)) 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: def test_phase1_execution_error_sets_failed_state(self, tmp_db, tmp_path): 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 raw = tmp_db.kv_get("clickup:task:task_err:state") state = json.loads(raw) assert state["state"] == "failed" def test_phase1_error_return_sets_failed(self, tmp_db, tmp_path): 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:") raw = tmp_db.kv_get("clickup:task:task_err2:state") state = json.loads(raw) assert state["state"] == "failed"