ppt-web / tests /test_enhanced_ppt_service_modularization.py
26fwyzpz6f-max
Clean deploy without binary files
6aecb2e
Raw
History Blame Contribute Delete
36.4 kB
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": "<!DOCTYPE html><html></html>",
}
@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": "<section>ok</section>",
}
selected = await service.get_selected_global_template("proj-1")
assert selected["template_name"] == "AI Free"
assert selected["html_template"] == "<section>ok</section>"
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 = "<html><head></head><body><div>Body</div></body></html>"
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("<html></html>", "- 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("<think>internal</think>", "").strip())
cleaned = clean_html_response(
owner,
"<think>internal</think>\nHere's the HTML code:\n```html\n<!DOCTYPE html>\n<html><body><div>ok</div></body></html>\n```"
)
assert cleaned.startswith("<!DOCTYPE html>")
assert cleaned.endswith("</html>")
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
<!DOCTYPE html>
<html>
<body>
<div class="metric">Runtime error rate dropped to 0.2%</div>
</body>
</html>
```"""
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
)