from pathlib import Path from types import SimpleNamespace import pytest 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") class _ProjectManagerStub: def __init__(self, project): self.project = project self.metadata_updates = [] async def get_project(self, project_id, user_id=None): return self.project async def update_project_metadata(self, project_id, metadata): copied = dict(metadata) self.metadata_updates.append(copied) self.project.project_metadata = copied class _GlobalTemplateServiceStub: def __init__(self): self.calls = [] async def generate_template_with_ai_stream(self, **kwargs): self.calls.append(kwargs) yield { "type": "thinking", "content": "前置说明```html\n
partial", } yield { "type": "complete", "message": "模板生成完成!", "template_name": "自由模板-proj1234", "html_template": "
done
", } class _TemplateSelectionServiceStub: def __init__(self, project): self.project_manager = _ProjectManagerStub(project) self.global_template_service = _GlobalTemplateServiceStub() self._free_template_generation_locks = {} self.cleared_projects = [] def clear_cached_style_genes(self, project_id=None): self.cleared_projects.append(project_id) class _JsonRequestStub: def __init__(self, payload): self._payload = payload async def json(self): return self._payload @pytest.mark.asyncio async def test_stream_free_template_generation_emits_preview_and_persists_final_html(): project = SimpleNamespace( topic="流式模板", outline={"slides": [{"title": "封面"}]}, confirmed_requirements={"style": "modern"}, requirements="请生成现代商务风格", description="演示流式模板预览", project_metadata={"template_mode": "free", "free_template_status": "pending"}, ) stub = _TemplateSelectionServiceStub(project) service = TemplateSelectionService(stub) events = [ event async for event in service.stream_free_template_generation("proj12345678", user_id=99, force=False) ] assert [event["type"] for event in events[:2]] == ["status", "status"] assert any(event["type"] == "preview" for event in events) assert events[-1]["type"] == "complete" assert events[-1]["template"]["html_template"].startswith("") assert stub.project_manager.metadata_updates[0]["free_template_status"] == "generating" final_metadata = stub.project_manager.metadata_updates[-1] assert final_metadata["free_template_status"] == "ready" assert final_metadata["free_template_name"] == "自由模板-proj1234" assert final_metadata["free_template_html"] == "
done
" assert final_metadata["free_template_prompt"] assert stub.cleared_projects == ["proj12345678"] assert stub.global_template_service.calls[0]["prompt_is_ready"] is True @pytest.mark.asyncio async def test_stream_free_template_generation_reuses_existing_template_without_regenerating(): project = SimpleNamespace( topic="流式模板", outline={}, confirmed_requirements={}, requirements="", description="", project_metadata={ "template_mode": "free", "free_template_status": "ready", "free_template_name": "已有模板", "free_template_html": "cached", }, ) stub = _TemplateSelectionServiceStub(project) service = TemplateSelectionService(stub) events = [ event async for event in service.stream_free_template_generation("proj12345678", user_id=99, force=False) ] assert [event["type"] for event in events] == ["status", "complete"] assert events[-1]["template_name"] == "已有模板" assert events[-1]["html_template"] == "cached" assert stub.project_manager.metadata_updates == [] assert stub.global_template_service.calls == [] def test_extract_free_template_preview_html_supports_partial_html_chunks(): preview = TemplateSelectionService._extract_free_template_preview_html( "说明文字```html\n
partial" ) assert preview == "
partial" @pytest.mark.asyncio async def test_confirm_route_persists_current_frontend_html_before_generation(monkeypatch): from landppt.web.route_modules import template_routes project = SimpleNamespace( topic="template-demo", project_metadata={ "template_mode": "free", "free_template_name": "first-version", "free_template_html": "first", "free_template_status": "ready", }, ) stub = _TemplateSelectionServiceStub(project) service = TemplateSelectionService(stub) async def _get_selected_global_template(project_id, user_id=None): return await service.get_selected_global_template(project_id, user_id=user_id) stub.get_selected_global_template = _get_selected_global_template monkeypatch.setattr(template_routes, "get_ppt_service_for_user", lambda user_id: stub) confirm_result = await template_routes.confirm_project_free_template( "proj-confirm", _JsonRequestStub( { "save_to_library": False, "template_name": "adjusted-version", "html_template": "adjusted", } ), user=SimpleNamespace(id=7), ) selected_result = await template_routes.get_selected_global_template( "proj-confirm", user=SimpleNamespace(id=7), ) assert confirm_result["success"] is True assert stub.project_manager.metadata_updates[-1]["free_template_html"] == "adjusted" assert stub.project_manager.metadata_updates[-1]["free_template_name"] == "adjusted-version" assert selected_result["template"]["html_template"] == "adjusted" assert stub.cleared_projects == ["proj-confirm"] def test_free_template_streaming_route_and_frontend_use_sse_preview_pipeline(): route_text = _read("src/landppt/web/route_modules/template_routes.py") service_text = _read("src/landppt/services/template/template_selection_service.py") script_text = _read("src/landppt/web/templates/components/project/todo_board_with_editor/script_1.html") assert 'want_stream = True if stream_flag is None else bool(stream_flag)' in route_text assert 'media_type="text/event-stream"' in route_text assert 'stream_free_template_generation(' in route_text assert '"type": "preview"' in service_text assert "'Accept': 'text/event-stream'" in script_text assert "consumeSseJsonStream(resp" in script_text assert 'iframe.srcdoc = freeTemplateCurrentHtml;' in script_text assert "html_template: getFreeTemplateHtmlForConfirm()" in script_text assert 'submitted_html = data.get("html_template")' in route_text