import ast import re from pathlib import Path from types import SimpleNamespace import pytest from landppt.services.slide.creative_design_service import CreativeDesignService from landppt.services.template.global_master_template_service import GlobalMasterTemplateService from landppt.services.slide.layout_repair_service import LayoutRepairService from landppt.services.slide.slide_html_cleanup_service import SlideHtmlCleanupService from landppt.services.slide.slide_generation_service import SlideGenerationService from landppt.services.template.template_selection_service import TemplateSelectionService ROOT = Path(__file__).resolve().parents[1] def _read(relative_path: str) -> str: return (ROOT / relative_path).read_text(encoding="utf-8") def _direct_class_methods(relative_path: str, class_name: str) -> set[str]: tree = ast.parse(_read(relative_path)) top_level_funcs = [ node.name for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) ] assert top_level_funcs == [] class_node = next( node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == class_name ) return { node.name for node in class_node.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) } def _load_class_method(relative_path: str, class_name: str, method_name: str): tree = ast.parse(_read(relative_path)) class_node = next( node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == class_name ) method_node = next( node for node in class_node.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == method_name ) module = ast.Module(body=[method_node], type_ignores=[]) ast.fix_missing_locations(module) class _Logger: def debug(self, *args, **kwargs): return None def warning(self, *args, **kwargs): return None def error(self, *args, **kwargs): return None namespace = {"logger": _Logger(), "re": re} exec(compile(module, relative_path, "exec"), namespace) return namespace[method_name] def test_enhanced_ppt_service_delegates_extracted_service_logic(): service_text = _read("src/landppt/services/enhanced_ppt_service.py") creative_text = _read("src/landppt/services/slide/creative_design_service.py") layout_text = _read("src/landppt/services/slide/layout_repair_service.py") outline_text = _read("src/landppt/services/outline/project_outline_workflow_service.py") outline_generation_text = _read("src/landppt/services/outline/project_outline_generation_service.py") outline_creation_text = _read("src/landppt/services/outline/project_outline_creation_service.py") outline_research_text = _read("src/landppt/services/outline/project_outline_research_service.py") runtime_text = _read("src/landppt/services/runtime/runtime_support_service.py") slide_authoring_text = _read("src/landppt/services/slide/slide_authoring_service.py") slide_stream_text = _read("src/landppt/services/slide/slide_streaming_service.py") slide_generation_text = _read("src/landppt/services/slide/slide_generation_service.py") template_text = _read("src/landppt/services/template/template_selection_service.py") assert "from .slide.creative_design_service import CreativeDesignService" in service_text assert "from .slide.layout_repair_service import LayoutRepairService" in service_text assert "from .outline.project_outline_workflow_service import ProjectOutlineWorkflowService" in service_text assert "from .runtime.runtime_support_service import RuntimeSupportService" in service_text assert "from .slide.slide_authoring_service import SlideAuthoringService" in service_text assert "from .slide.slide_generation_service import SlideGenerationService" in service_text assert "from .template.template_selection_service import TemplateSelectionService" in service_text assert "import asyncio" in slide_generation_text assert "self.creative_design = CreativeDesignService(self)" in service_text assert "self.runtime_support = RuntimeSupportService(self)" in service_text assert "self.project_outline_workflow = ProjectOutlineWorkflowService(self)" in service_text assert "self.slide_authoring = SlideAuthoringService(self)" in service_text assert "self.slide_generation = SlideGenerationService(self)" in service_text assert "self.layout_repair = LayoutRepairService(self)" in service_text assert "self.template_selection = TemplateSelectionService(self)" in service_text assert "self.enhanced_research_service = None" in service_text assert "self.enhanced_report_generator = None" in service_text assert "self._initialize_research_services()" in service_text assert "self.runtime_support._initialize_image_service()" in service_text assert "async for chunk in self.slide_generation._generate_slides_streaming_impl(project_id):" in service_text assert "return await self.creative_design._generate_slide_with_template(" in service_text assert "return await self.creative_design._get_creative_design_inputs(" in service_text assert "return LayoutRepairService._should_skip_layout_repair(inspection_report)" in service_text assert "return await self.layout_repair._apply_auto_layout_repair(" in service_text assert "return await self.template_selection.get_selected_global_template(project_id, user_id=user_id)" in service_text assert "return self.creative_design.clear_cached_style_genes(project_id)" in service_text assert "return self.runtime_support._build_execution_context(role, current_ai_config)" in service_text assert "return self.runtime_support.iter_research_stream_payloads(event)" in service_text assert "return await self.project_outline_workflow.create_project_with_workflow(request)" in service_text assert "async for item in self.project_outline_workflow.generate_outline_streaming(" in service_text assert "force_regenerate=force_regenerate" in service_text assert "async for item in self.slide_authoring.generate_slides_streaming(project_id):" in service_text assert "return self.slide_authoring._clean_html_response(raw_content)" in service_text assert "return await self.slide_authoring.regenerate_slide(project_id, slide_index, request)" in service_text assert len(service_text.splitlines()) < 1000 for marker in [ "get_combined_style_genes_and_guide_prompt(", "current_time_context = self._build_current_time_prompt_context()", "credits_should_bill = False", "with tempfile.TemporaryDirectory() as tmp_dir:", "conduct_enhanced_research(", "self._slides_generation_cancel_key(project_id)", ]: assert marker not in service_text assert "get_combined_style_genes_and_guide_prompt(" in creative_text assert "with tempfile.TemporaryDirectory() as tmp_dir:" in layout_text assert "conduct_enhanced_research(" in outline_research_text assert "def _build_execution_context(" in runtime_text assert "self._slides_generation_cancel_key(project_id)" in slide_stream_text assert "async def generate_slides_streaming(self, project_id: str):" in slide_authoring_text assert "credits_should_bill = False" in slide_generation_text assert 'template_name = f"' in template_text assert "project_id[:8]" in template_text def test_split_workflow_runtime_and_slide_services_are_class_based_and_delegated(): project_wrapper_text = _read("src/landppt/services/outline/project_outline_workflow_service.py") project_generation_text = _read("src/landppt/services/outline/project_outline_generation_service.py") project_stage_text = _read("src/landppt/services/project_workflow_stage_service.py") slide_wrapper_text = _read("src/landppt/services/slide/slide_authoring_service.py") slide_html_text = _read("src/landppt/services/slide/slide_html_service.py") slide_cleanup_text = _read("src/landppt/services/slide/slide_html_cleanup_service.py") slide_stream_text = _read("src/landppt/services/slide/slide_streaming_service.py") runtime_wrapper_text = _read("src/landppt/services/runtime/runtime_support_service.py") runtime_research_text = _read("src/landppt/services/runtime/runtime_research_service.py") runtime_ai_text = _read("src/landppt/services/runtime/runtime_ai_service.py") runtime_image_text = _read("src/landppt/services/runtime/runtime_image_service.py") assert "from .project_outline_generation_service import ProjectOutlineGenerationService" in project_wrapper_text assert "from ..project_workflow_stage_service import ProjectWorkflowStageService" in project_wrapper_text assert "self._outline_generation = ProjectOutlineGenerationService(self)" in project_wrapper_text assert "self._workflow_stage = ProjectWorkflowStageService(self)" in project_wrapper_text assert "from .slide_html_service import SlideHtmlService" in slide_wrapper_text assert "from .slide_streaming_service import SlideStreamingService" in slide_wrapper_text assert "self._html_service = SlideHtmlService(self)" in slide_wrapper_text assert "self._streaming_service = SlideStreamingService(self)" in slide_wrapper_text assert "return self._html_service._clean_html_response(raw_content)" in slide_wrapper_text assert "from .slide_html_cleanup_service import SlideHtmlCleanupService" in slide_html_text assert "self._cleanup_service = SlideHtmlCleanupService(self)" in slide_html_text assert "from .runtime_research_service import RuntimeResearchService" in runtime_wrapper_text assert "from .runtime_ai_service import RuntimeAIService" in runtime_wrapper_text assert "from .runtime_image_service import RuntimeImageService" in runtime_wrapper_text assert "self._research_runtime = RuntimeResearchService(self)" in runtime_wrapper_text assert "self._ai_runtime = RuntimeAIService(self)" in runtime_wrapper_text assert "self._image_runtime = RuntimeImageService(self)" in runtime_wrapper_text assert "def _owner(self):" in runtime_image_text assert "return self._service._service" in runtime_image_text assert "self._owner.image_service = ImageService(image_config)" in runtime_image_text assert "def _owner(self):" in runtime_research_text assert "self._owner.enhanced_research_service = EnhancedResearchService(user_id=self.user_id)" in runtime_research_text assert "self._owner.enhanced_report_generator = EnhancedReportGenerator" in runtime_research_text assert "self._owner.research_service = DEEPResearchService(user_id=self.user_id)" in runtime_research_text assert "self._owner.report_generator = ResearchReportGenerator" in runtime_research_text assert len(project_wrapper_text.splitlines()) < 250 assert len(slide_wrapper_text.splitlines()) < 250 assert len(runtime_wrapper_text.splitlines()) < 220 project_methods = _direct_class_methods( "src/landppt/services/outline/project_outline_workflow_service.py", "ProjectOutlineWorkflowService", ) slide_methods = _direct_class_methods( "src/landppt/services/slide/slide_authoring_service.py", "SlideAuthoringService", ) runtime_methods = _direct_class_methods( "src/landppt/services/runtime/runtime_support_service.py", "RuntimeSupportService", ) assert {"generate_outline_streaming", "_execute_outline_generation", "confirm_project_outline"} <= project_methods assert {"generate_slides_streaming", "_generate_fallback_slide_html", "regenerate_slide"} <= slide_methods assert {"iter_research_stream_payloads", "_build_execution_context", "_initialize_image_service"} <= runtime_methods assert "async def generate_outline_streaming" in project_generation_text assert "async def confirm_project_outline" in project_stage_text assert "def _generate_fallback_slide_html" in slide_html_text assert "def _clean_html_response" in slide_cleanup_text assert "async def generate_slides_streaming" in slide_stream_text assert "def iter_research_stream_payloads" in runtime_research_text assert "def _build_execution_context" in runtime_ai_text assert "def _initialize_image_service" in runtime_image_text def test_second_level_service_splits_are_facades_and_subservices_are_class_based(): project_generation_text = _read("src/landppt/services/outline/project_outline_generation_service.py") project_creation_text = _read("src/landppt/services/outline/project_outline_creation_service.py") project_streaming_text = _read("src/landppt/services/outline/project_outline_streaming_service.py") project_validation_text = _read("src/landppt/services/outline/project_outline_validation_service.py") project_page_count_text = _read("src/landppt/services/outline/project_outline_page_count_service.py") slide_html_text = _read("src/landppt/services/slide/slide_html_service.py") slide_cleanup_text = _read("src/landppt/services/slide/slide_html_cleanup_service.py") slide_content_text = _read("src/landppt/services/slide/slide_content_service.py") slide_media_text = _read("src/landppt/services/slide/slide_media_service.py") slide_validation_text = _read("src/landppt/services/slide/slide_html_validation_service.py") slide_document_text = _read("src/landppt/services/slide/slide_document_service.py") runtime_ai_text = _read("src/landppt/services/runtime/runtime_ai_service.py") runtime_provider_text = _read("src/landppt/services/runtime/runtime_provider_service.py") runtime_config_text = _read("src/landppt/services/runtime/runtime_config_service.py") runtime_maintenance_text = _read("src/landppt/services/runtime/runtime_maintenance_service.py") assert "from .project_outline_creation_service import ProjectOutlineCreationService" in project_generation_text assert "from .project_outline_streaming_service import ProjectOutlineStreamingService" in project_generation_text assert "from .project_outline_validation_service import ProjectOutlineValidationService" in project_generation_text assert "from .project_outline_page_count_service import ProjectOutlinePageCountService" in project_generation_text assert "self._creation_service = ProjectOutlineCreationService(self)" in project_generation_text assert "self._streaming_service = ProjectOutlineStreamingService(self)" in project_generation_text assert "self._validation_service = ProjectOutlineValidationService(self)" in project_generation_text assert "self._page_count_service = ProjectOutlinePageCountService(self)" in project_generation_text assert "from .slide_content_service import SlideContentService" in slide_html_text assert "from .slide_media_service import SlideMediaService" in slide_html_text assert "from .slide_html_cleanup_service import SlideHtmlCleanupService" in slide_html_text assert "from .slide_html_validation_service import SlideHtmlValidationService" in slide_html_text assert "from .slide_document_service import SlideDocumentService" in slide_html_text assert "self._content_service = SlideContentService(self)" in slide_html_text assert "self._media_service = SlideMediaService(self)" in slide_html_text assert "self._cleanup_service = SlideHtmlCleanupService(self)" in slide_html_text assert "self._validation_service = SlideHtmlValidationService(self)" in slide_html_text assert "self._document_service = SlideDocumentService(self)" in slide_html_text assert "def _clean_html_response(self, raw_content: str) -> str:" in slide_cleanup_text assert "from .runtime_provider_service import RuntimeProviderService" in runtime_ai_text assert "from .runtime_config_service import RuntimeConfigService" in runtime_ai_text assert "from .runtime_maintenance_service import RuntimeMaintenanceService" in runtime_ai_text assert "self._provider_service = RuntimeProviderService(self)" in runtime_ai_text assert "self._config_service = RuntimeConfigService(self)" in runtime_ai_text assert "self._maintenance_service = RuntimeMaintenanceService(self)" in runtime_ai_text assert "@property" in runtime_ai_text assert "return self._provider_service.ai_provider" in runtime_ai_text assert len(project_generation_text.splitlines()) < 180 assert len(slide_html_text.splitlines()) < 200 assert len(slide_cleanup_text.splitlines()) < 120 assert len(runtime_ai_text.splitlines()) < 160 project_generation_methods = _direct_class_methods( "src/landppt/services/outline/project_outline_generation_service.py", "ProjectOutlineGenerationService", ) slide_html_methods = _direct_class_methods( "src/landppt/services/slide/slide_html_service.py", "SlideHtmlService", ) runtime_ai_methods = _direct_class_methods( "src/landppt/services/runtime/runtime_ai_service.py", "RuntimeAIService", ) assert {"generate_outline_streaming", "_execute_outline_generation", "_validate_outline_structure"} <= project_generation_methods assert {"_generate_single_slide_html_with_prompts", "_generate_fallback_slide_html", "_generate_html_with_retry"} <= slide_html_methods assert {"ai_provider", "_text_completion_for_role", "_configure_summeryfile_api"} <= runtime_ai_methods assert "async def generate_outline" in project_creation_text assert "async def generate_outline_streaming" in project_streaming_text assert "async def _validate_and_repair_outline_json" in project_validation_text assert "async def _execute_outline_generation" in project_page_count_text assert "async def generate_slides_parallel" in slide_content_text assert "async def _generate_single_slide_html_with_prompts" in slide_media_text assert "async def _generate_html_with_retry" in slide_validation_text assert "def _generate_fallback_slide_html" in slide_document_text assert "def _clean_html_response" in slide_cleanup_text assert "@property" in runtime_provider_text assert "async def _get_current_ai_config_async" in runtime_config_text assert "def get_cache_stats" in runtime_maintenance_text _direct_class_methods("src/landppt/services/outline/project_outline_creation_service.py", "ProjectOutlineCreationService") _direct_class_methods("src/landppt/services/outline/project_outline_streaming_service.py", "ProjectOutlineStreamingService") _direct_class_methods("src/landppt/services/outline/project_outline_validation_service.py", "ProjectOutlineValidationService") _direct_class_methods("src/landppt/services/outline/project_outline_page_count_service.py", "ProjectOutlinePageCountService") _direct_class_methods("src/landppt/services/slide/slide_content_service.py", "SlideContentService") _direct_class_methods("src/landppt/services/slide/slide_media_service.py", "SlideMediaService") _direct_class_methods("src/landppt/services/slide/slide_html_cleanup_service.py", "SlideHtmlCleanupService") _direct_class_methods("src/landppt/services/slide/slide_html_validation_service.py", "SlideHtmlValidationService") _direct_class_methods("src/landppt/services/slide/slide_document_service.py", "SlideDocumentService") _direct_class_methods("src/landppt/services/runtime/runtime_provider_service.py", "RuntimeProviderService") _direct_class_methods("src/landppt/services/runtime/runtime_config_service.py", "RuntimeConfigService") _direct_class_methods("src/landppt/services/runtime/runtime_maintenance_service.py", "RuntimeMaintenanceService") def test_third_level_service_splits_keep_creation_validation_and_html_validation_thin(): project_creation_text = _read("src/landppt/services/outline/project_outline_creation_service.py") project_prompt_text = _read("src/landppt/services/outline/project_outline_prompt_service.py") project_research_text = _read("src/landppt/services/outline/project_outline_research_service.py") project_validation_text = _read("src/landppt/services/outline/project_outline_validation_service.py") project_repair_text = _read("src/landppt/services/outline/project_outline_repair_service.py") project_normalization_text = _read("src/landppt/services/outline/project_outline_normalization_service.py") slide_validation_text = _read("src/landppt/services/slide/slide_html_validation_service.py") slide_inspection_text = _read("src/landppt/services/slide/slide_html_inspection_service.py") slide_recovery_text = _read("src/landppt/services/slide/slide_html_recovery_service.py") assert "from .project_outline_prompt_service import ProjectOutlinePromptService" in project_creation_text assert "from .project_outline_research_service import ProjectOutlineResearchService" in project_creation_text assert "self._prompt_service = ProjectOutlinePromptService(self)" in project_creation_text assert "self._research_service = ProjectOutlineResearchService(self)" in project_creation_text assert "from .project_outline_repair_service import ProjectOutlineRepairService" in project_validation_text assert "from .project_outline_normalization_service import ProjectOutlineNormalizationService" in project_validation_text assert "self._repair_service = ProjectOutlineRepairService(self)" in project_validation_text assert "self._normalization_service = ProjectOutlineNormalizationService(self)" in project_validation_text assert "from .slide_html_inspection_service import SlideHtmlInspectionService" in slide_validation_text assert "from .slide_html_recovery_service import SlideHtmlRecoveryService" in slide_validation_text assert "self._inspection_service = SlideHtmlInspectionService(self)" in slide_validation_text assert "self._recovery_service = SlideHtmlRecoveryService(self)" in slide_validation_text assert len(project_creation_text.splitlines()) < 120 assert len(project_validation_text.splitlines()) < 120 assert len(slide_validation_text.splitlines()) < 100 project_creation_methods = _direct_class_methods( "src/landppt/services/outline/project_outline_creation_service.py", "ProjectOutlineCreationService", ) project_validation_methods = _direct_class_methods( "src/landppt/services/outline/project_outline_validation_service.py", "ProjectOutlineValidationService", ) slide_validation_methods = _direct_class_methods( "src/landppt/services/slide/slide_html_validation_service.py", "SlideHtmlValidationService", ) assert {"generate_outline", "_create_outline_prompt", "conduct_research_and_merge_with_files"} <= project_creation_methods assert {"_validate_and_repair_outline_json", "_parse_outline_content", "_update_outline_generation_stage"} <= project_validation_methods assert {"_validate_html_completeness", "_generate_html_with_retry", "_fix_incomplete_html"} <= slide_validation_methods assert "def _create_outline_prompt" in project_prompt_text assert "async def generate_outline" in project_research_text assert "async def _validate_and_repair_outline_json" in project_repair_text assert "def _parse_outline_content" in project_normalization_text assert "def _validate_html_completeness" in slide_inspection_text assert "async def _generate_html_with_retry" in slide_recovery_text _direct_class_methods("src/landppt/services/outline/project_outline_prompt_service.py", "ProjectOutlinePromptService") _direct_class_methods("src/landppt/services/outline/project_outline_research_service.py", "ProjectOutlineResearchService") _direct_class_methods("src/landppt/services/outline/project_outline_repair_service.py", "ProjectOutlineRepairService") _direct_class_methods("src/landppt/services/outline/project_outline_normalization_service.py", "ProjectOutlineNormalizationService") _direct_class_methods("src/landppt/services/slide/slide_html_inspection_service.py", "SlideHtmlInspectionService") _direct_class_methods("src/landppt/services/slide/slide_html_recovery_service.py", "SlideHtmlRecoveryService") class _DummyProjectManager: def __init__(self, project): self.project = project self.updated = [] async def get_project(self, project_id, user_id=None): del project_id, user_id return self.project async def update_project_metadata(self, project_id, metadata): self.updated.append((project_id, dict(metadata))) self.project.project_metadata = dict(metadata) class _DummyGlobalTemplateService: def __init__(self): self.incremented = [] self.generated_kwargs = [] async def get_template_by_id(self, template_id): return {"id": template_id, "template_name": "Global", "is_active": True} async def get_default_template(self): return {"id": 9, "template_name": "Default", "is_active": True} async def increment_template_usage(self, template_id): self.incremented.append(template_id) async def generate_template_with_ai(self, **kwargs): self.generated_kwargs.append(dict(kwargs)) return { "template_name": kwargs["template_name"], "html_template": "", } @pytest.mark.asyncio async def test_template_selection_service_updates_project_metadata_and_returns_cached_free_template(): project = SimpleNamespace( project_metadata={"selected_global_template_id": 1}, outline={"slides": []}, confirmed_requirements={}, topic="Quarterly Review", scenario="general", ) owner = SimpleNamespace( project_manager=_DummyProjectManager(project), global_template_service=_DummyGlobalTemplateService(), _free_template_generation_locks={}, cleared_projects=[], ) def _clear_cached_style_genes(project_id=None): owner.cleared_projects.append(project_id) owner.clear_cached_style_genes = _clear_cached_style_genes owner._build_current_time_prompt_context = lambda: "2026-03-28 12:00:00 CST" service = TemplateSelectionService(owner) result = await service.select_free_template_for_project("proj-1", user_id=7) assert result["success"] is True assert owner.project_manager.updated[-1][1]["template_mode"] == "free" assert owner.project_manager.updated[-1][1]["free_template_status"] == "pending" assert owner.cleared_projects == ["proj-1"] project.project_metadata = { "template_mode": "free", "free_template_name": "AI Free", "free_template_html": "
ok
", } selected = await service.get_selected_global_template("proj-1") assert selected["template_name"] == "AI Free" assert selected["html_template"] == "
ok
" assert selected["created_by"] == "ai_free" @pytest.mark.asyncio async def test_template_selection_service_uses_prebuilt_free_template_prompt_once(): project = SimpleNamespace( project_metadata={"template_mode": "free"}, outline={"slides": [{"title": "封面", "content_points": ["目标", "进展"]}]}, confirmed_requirements={"target_audience": "管理层"}, topic="Quarterly Review", scenario="general", ) global_template_service = _DummyGlobalTemplateService() owner = SimpleNamespace( project_manager=_DummyProjectManager(project), global_template_service=global_template_service, _free_template_generation_locks={}, cleared_projects=[], ) owner.clear_cached_style_genes = lambda project_id=None: owner.cleared_projects.append(project_id) owner._build_current_time_prompt_context = lambda: "2026-03-28 12:00:00 CST" service = TemplateSelectionService(owner) selected = await service.get_selected_global_template("proj-2") assert selected["template_name"].startswith("自由模板-") assert global_template_service.generated_kwargs kwargs = global_template_service.generated_kwargs[-1] assert kwargs["prompt_is_ready"] is True assert kwargs["generation_mode"] == "text_only" assert kwargs["prompt"].count("**创意要求**") == 1 assert kwargs["prompt"].count("**技术要求**") == 1 assert kwargs["prompt"].count("直接输出完整 HTML 模板") == 1 def test_creative_design_service_clears_owner_caches_and_files(tmp_path): cache_dir = tmp_path / "style_genes" cache_dir.mkdir() for filename in [ "proj-1_style_genes.json", "proj-1_combined_genes_guide.json", "proj-1_creative_guide.json", ]: (cache_dir / filename).write_text("{}", encoding="utf-8") owner = SimpleNamespace( cache_dirs={"style_genes": cache_dir}, _cached_style_genes={"proj-1": "genes"}, _cached_style_genes_and_guide={"proj-1": {"style_genes": "genes", "design_guide": "guide"}}, _cached_project_creative_guides={"proj-1": {"design_guide": "guide", "source_hash": "hash"}}, _cached_slide_creative_guides={"proj-1:1": "slide-guide"}, _style_genes_ready_events={"proj-1": object()}, _project_creative_guidance_ready_events={"proj-1": object()}, _slide_creative_guide_ready_events={"proj-1:1": object()}, ) service = CreativeDesignService(owner) summary = service._build_creative_slides_summary( [ {"title": "Intro", "slide_type": "title", "content_points": ["A", "B"]}, {"title": "Data", "slide_type": "content", "content_points": ["C"]}, ] ) assert "1. Intro" in summary assert "2. Data" in summary service.clear_cached_style_genes("proj-1") assert owner._cached_style_genes == {} assert owner._cached_style_genes_and_guide == {} assert owner._cached_project_creative_guides == {} assert owner._cached_slide_creative_guides == {} assert owner._style_genes_ready_events == {} assert owner._project_creative_guidance_ready_events == {} assert owner._slide_creative_guide_ready_events == {} assert not any(cache_dir.iterdir()) assert service.get_cached_style_genes_info()["total_count"] == 0 def test_layout_repair_service_helpers_keep_skip_logic_and_prompt_contract(): service = LayoutRepairService(SimpleNamespace()) low_report = "- severity: low" mixed_report = "- severity: medium" html = "
Body
" injected_html = service._inject_anti_overflow_css(html) assert service._should_skip_layout_repair(low_report) is True assert service._should_skip_layout_repair(mixed_report) is False assert "anti-overflow-fix" in injected_html assert "text-overflow: unset" in injected_html assert '[class*="card"] > *' in injected_html assert ".content-layer," in injected_html prompt = service._build_layout_repair_prompt("", "- issues: overflow") assert "```html" in prompt assert "overflow" in prompt assert "只有当页码越界、贴边、换行、漂移、被遮挡或安全区被侵占时" in prompt assert "不得改回 flex/grid 正文流" in prompt assert "inline style 表达了与上述相同的页码独立定位关系" in prompt def test_template_generation_prompts_require_stable_page_number_anchor(): owner = SimpleNamespace(_build_current_time_prompt_context=lambda: "2026-03-28 12:00:00 CST") service = TemplateSelectionService(owner) prompt = service._build_free_template_prompt( project=SimpleNamespace(topic="年度复盘", scenario="general"), outline={"slides": [{"title": "封面", "content_points": ["目标", "进展"]}]}, confirmed={}, ) assert "只输出一个 1280×720 的通用页面模板" in prompt assert "标题锚点区、主舞台区和编号锚点区" in prompt assert "不要输出 cover/toc/transition/content/title/ending 等多页面类型变体" in prompt assert prompt.count("**创意愿景——单页母版**") == 1 assert prompt.count("**技术要求**") == 1 template_prompt = GlobalMasterTemplateService._get_template_annotation_prompt_text() assert "只生成一个 1280×720 的通用页面模板" in template_prompt assert "使用 inline style 做等价实现同样有效" in template_prompt assert "画布根容器负责 `position:relative` 与 1280×720 裁切" in template_prompt creative_prompt = GlobalMasterTemplateService._get_template_generation_creative_prompt_text() assert "**创意愿景——单页母版**" in creative_prompt assert "不要分别生成页面类型变体" in creative_prompt def test_slide_generation_service_delegates_owner_attributes(): owner = SimpleNamespace(example_value="ok") service = SlideGenerationService(owner) assert service.example_value == "ok" def test_slide_html_service_cleans_markdown_wrapped_html_response(): clean_html_response = _load_class_method( "src/landppt/services/slide/slide_html_cleanup_service.py", "SlideHtmlCleanupService", "_clean_html_response", ) owner = SimpleNamespace(_strip_think_tags=lambda raw: raw.replace("internal", "").strip()) cleaned = clean_html_response( owner, "internal\nHere's the HTML code:\n```html\n\n
ok
\n```" ) assert cleaned.startswith("") assert cleaned.endswith("") assert "internal" not in cleaned def test_slide_html_cleanup_does_not_warn_for_valid_html_with_error_like_text(caplog): owner = SimpleNamespace(_strip_think_tags=lambda raw: raw.strip()) service = SlideHtmlCleanupService(owner) response = """```html
Runtime error rate dropped to 0.2%
```""" with caplog.at_level("WARNING", logger="landppt.services.slide.slide_html_cleanup_service"): cleaned = service._clean_html_response(response) assert "Runtime error rate dropped" in cleaned assert "AI response appears to be an error message instead of HTML" not in caplog.text def test_slide_html_cleanup_warns_for_plain_error_text(caplog): owner = SimpleNamespace(_strip_think_tags=lambda raw: raw.strip()) service = SlideHtmlCleanupService(owner) with caplog.at_level("WARNING", logger="landppt.services.slide.slide_html_cleanup_service"): cleaned = service._clean_html_response("Sorry, I cannot generate HTML for this slide.") assert cleaned == "" assert "AI response appears to be an error message instead of HTML" in caplog.text def _test_template_generation_prompts_require_stable_page_number_anchor_current(): owner = SimpleNamespace(_build_current_time_prompt_context=lambda: "2026-03-28 12:00:00 CST") service = TemplateSelectionService(owner) prompt = service._build_free_template_prompt( project=SimpleNamespace(topic="Annual review", scenario="general"), outline={"slides": [{"title": "Cover", "content_points": ["Goal", "Progress"]}]}, confirmed={}, ) assert "只输出一个 1280×720 的通用页面模板" in prompt assert "标题锚点区、主舞台区和编号锚点区" in prompt assert "编号锚点可省略或极简,但不能挤占正文" in prompt assert "不要输出 cover/toc/transition/content/title/ending 等多页面类型变体" in prompt template_prompt = GlobalMasterTemplateService._get_template_annotation_prompt_text() assert "只生成一个 1280×720 的通用页面模板" in template_prompt assert "标题锚点区、主舞台区、编号锚点区三类职责层" in template_prompt assert "类名仅用于说明结构关系,使用 inline style 做等价实现同样有效" in template_prompt creative_prompt = GlobalMasterTemplateService._get_template_generation_creative_prompt_text() assert "**创意愿景——单页母版**" in creative_prompt assert "不要分别生成页面类型变体" in creative_prompt test_template_generation_prompts_require_stable_page_number_anchor = ( _test_template_generation_prompts_require_stable_page_number_anchor_current )