Files
LandPPT/tests/test_free_template_streaming.py
sligter 6153e56441 feat: modularize services and expand platform workflows
Add OAuth, community, credits, startup, export, and narration workflows while splitting oversized route and PPT service modules into dedicated components with broader regression coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:04:37 +08:00

198 lines
7.5 KiB
Python

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<!DOCTYPE html><html><body><div class='preview'>partial",
}
yield {
"type": "complete",
"message": "模板生成完成!",
"template_name": "自由模板-proj1234",
"html_template": "<!DOCTYPE html><html><body><div class='final'>done</div></body></html>",
}
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("<!DOCTYPE html>")
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"] == "<!DOCTYPE html><html><body><div class='final'>done</div></body></html>"
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": "<!DOCTYPE html><html><body>cached</body></html>",
},
)
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"] == "<!DOCTYPE html><html><body>cached</body></html>"
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<!DOCTYPE html><html><body><div class='preview'>partial"
)
assert preview == "<!DOCTYPE html><html><body><div class='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": "<!DOCTYPE html><html><body>first</body></html>",
"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": "<!DOCTYPE html><html><body>adjusted</body></html>",
}
),
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"] == "<!DOCTYPE html><html><body>adjusted</body></html>"
assert stub.project_manager.metadata_updates[-1]["free_template_name"] == "adjusted-version"
assert selected_result["template"]["html_template"] == "<!DOCTYPE html><html><body>adjusted</body></html>"
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