mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-28 23:31:06 +08:00
6153e56441
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>
198 lines
7.5 KiB
Python
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
|