Files
LandPPT/tests/test_creative_guidance_defaults.py
sligter 152876610a feat(research): add custom Tavily base URL configuration support
Add optional TAVILY_BASE_URL environment variable and configuration option to enable custom Tavily API endpoints, such as proxies or private gateways. Updated core configuration, research service initialization, UI templates, and tests to handle the new base URL parameter. Also enhanced outline streaming with force_regenerate option for improved regeneration control.
2026-04-16 21:37:38 +08:00

363 lines
14 KiB
Python

import asyncio
import json
from pathlib import Path
from types import SimpleNamespace
def test_config_service_defaults_enable_per_slide_creative_guidance():
from landppt.services.config_service import ConfigService
service = ConfigService(env_file=".env.example")
assert service.config_schema["enable_per_slide_creative_guidance"]["default"] == "true"
def test_db_config_service_defaults_enable_per_slide_creative_guidance():
from landppt.services.db_config_service import DatabaseConfigService
service = DatabaseConfigService()
assert service.config_schema["enable_per_slide_creative_guidance"]["default"] == "true"
def test_tavily_base_url_exists_in_config_schemas():
from landppt.services.config_service import ConfigService
from landppt.services.db_config_service import DatabaseConfigService
env_service = ConfigService(env_file=".env.example")
db_service = DatabaseConfigService()
assert env_service.config_schema["tavily_base_url"]["type"] == "url"
assert env_service.config_schema["tavily_base_url"]["category"] == "generation_params"
assert env_service.config_schema["tavily_base_url"]["default"] == "https://api.tavily.com"
assert db_service.config_schema["tavily_base_url"]["type"] == "url"
assert db_service.config_schema["tavily_base_url"]["category"] == "generation_params"
assert db_service.config_schema["tavily_base_url"]["default"] == "https://api.tavily.com"
def test_db_config_service_resolves_blank_tavily_base_url_to_official_default():
from landppt.services.db_config_service import DatabaseConfigService
service = DatabaseConfigService()
resolved = service._resolve_config_values(
{
"tavily_base_url": {
"value": "",
"type": "url",
"category": "generation_params",
"is_user_override": True,
},
},
{},
)
assert resolved["tavily_base_url"] == "https://api.tavily.com"
def test_apryse_standard_pptx_export_is_admin_only_in_config_schema():
from landppt.services.config_service import ConfigService
from landppt.services.db_config_service import DatabaseConfigService
env_service = ConfigService(env_file=".env.example")
db_service = DatabaseConfigService()
assert env_service.config_schema["enable_apryse_pptx_export"]["default"] == "false"
assert env_service.config_schema["enable_apryse_pptx_export"]["admin_only"] is True
assert env_service.config_schema["apryse_license_key"]["admin_only"] is True
assert db_service.config_schema["enable_apryse_pptx_export"]["default"] == "false"
assert db_service.config_schema["enable_apryse_pptx_export"]["admin_only"] is True
assert db_service.config_schema["apryse_license_key"]["admin_only"] is True
def test_db_config_service_filters_and_resolves_admin_only_apryse_config(monkeypatch):
from landppt.services.db_config_service import DatabaseConfigService
service = DatabaseConfigService()
public_schema = service.get_config_schema(include_admin_only=False)
resolved = service._resolve_config_values(
{
"apryse_license_key": {"value": "user-license", "type": "password", "category": "generation_params"},
"enable_apryse_pptx_export": {"value": "false", "type": "boolean", "category": "generation_params"},
},
{
"apryse_license_key": {"value": "system-license", "type": "password", "category": "generation_params"},
"enable_apryse_pptx_export": {"value": "true", "type": "boolean", "category": "generation_params"},
},
)
async def fake_get_all_config(user_id=None):
return {
"apryse_license_key": "system-license",
"enable_apryse_pptx_export": True,
"openai_model": "gpt-4.1",
}
monkeypatch.setattr(service, "get_all_config", fake_get_all_config)
filtered = asyncio.run(service.get_all_config_for_user(user_id=1, is_admin=False))
assert "apryse_license_key" not in public_schema
assert "enable_apryse_pptx_export" not in public_schema
assert resolved["apryse_license_key"] == "system-license"
assert resolved["enable_apryse_pptx_export"] is True
assert "apryse_license_key" not in filtered
assert "enable_apryse_pptx_export" not in filtered
assert filtered["openai_model"] == "gpt-4.1"
def test_db_config_service_resolves_system_scope_admin_only_config_from_merged_rows():
from landppt.services.db_config_service import DatabaseConfigService
service = DatabaseConfigService()
resolved = service._resolve_config_values(
{
"apryse_license_key": {
"value": "system-license",
"type": "password",
"category": "generation_params",
"is_user_override": False,
},
"enable_apryse_pptx_export": {
"value": "true",
"type": "boolean",
"category": "generation_params",
"is_user_override": False,
},
},
{},
)
assert resolved["apryse_license_key"] == "system-license"
assert resolved["enable_apryse_pptx_export"] is True
def test_db_config_service_ignores_user_override_for_admin_only_when_no_system_default():
from landppt.services.db_config_service import DatabaseConfigService
service = DatabaseConfigService()
resolved = service._resolve_config_values(
{
"apryse_license_key": {
"value": "legacy-user-license",
"type": "password",
"category": "generation_params",
"is_user_override": True,
},
"enable_apryse_pptx_export": {
"value": "true",
"type": "boolean",
"category": "generation_params",
"is_user_override": True,
},
},
{},
)
assert resolved["apryse_license_key"] in (None, "")
assert resolved["enable_apryse_pptx_export"] is False
def test_db_config_service_migrates_legacy_system_default_to_true(monkeypatch):
import landppt.database.database as database_mod
import landppt.database.repositories as repo_mod
from landppt.services.db_config_service import DatabaseConfigService
captured = {"set_calls": []}
class FakeSession:
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
async def commit(self):
return None
class FakeRepo:
def __init__(self, session):
self.session = session
async def get_all_configs(self, user_id=None):
return {
"enable_per_slide_creative_guidance": {"value": "false"},
}
async def set_config(self, **kwargs):
captured["set_calls"].append(kwargs)
monkeypatch.setattr(database_mod, "AsyncSessionLocal", lambda: FakeSession())
monkeypatch.setattr(repo_mod, "UserConfigRepository", FakeRepo)
service = DatabaseConfigService()
updated = asyncio.run(service.initialize_system_defaults())
assert updated >= 1
assert any(
call["key"] == "enable_per_slide_creative_guidance" and call["value"] == "true"
for call in captured["set_calls"]
)
def test_creative_design_service_uses_slide_prompt_when_per_slide_guidance_enabled(tmp_path):
from landppt.services.slide.creative_design_service import CreativeDesignService
class _DummyService:
def __init__(self):
self.user_id = 1
self.cache_dirs = {"style_genes": tmp_path}
self._cached_style_genes = {}
self._cached_global_constitutions = {}
self._cached_page_creative_briefs = {}
self._cached_slide_creative_guides = {}
self._style_genes_ready_events = {}
self._global_constitution_ready_events = {}
self._page_creative_brief_ready_events = {}
self._slide_creative_guide_ready_events = {}
async def _get_user_generation_config(self):
return {"enable_per_slide_creative_guidance": True}
owner = _DummyService()
service = CreativeDesignService(owner)
async def fake_style_genes(project_id, template_html, page_number):
return "STYLE-GENES"
async def fake_constitution(*args, **kwargs):
return "GLOBAL-CONSTITUTION"
async def fail_page_briefs(*args, **kwargs):
raise AssertionError("page-type briefs should not be used when per-slide guidance is enabled")
async def fake_slide_guide(*args, **kwargs):
return "SLIDE-GUIDE"
service._get_or_extract_style_genes = fake_style_genes
service._get_or_generate_global_constitution = fake_constitution
service._get_or_generate_page_creative_briefs = fail_page_briefs
service._get_or_generate_slide_creative_guide = fake_slide_guide
result = asyncio.run(
service._get_creative_design_inputs(
project_id="proj-1",
template_html="<div></div>",
slide_data={"title": "增长机会"},
page_number=2,
total_pages=5,
confirmed_requirements={"topic": "年度复盘"},
all_slides=[{"title": "封面"}, {"title": "增长机会"}],
)
)
assert result == ("STYLE-GENES", "GLOBAL-CONSTITUTION", "SLIDE-GUIDE")
def test_creative_design_service_uses_page_type_briefs_when_per_slide_guidance_disabled(tmp_path):
from landppt.services.slide.creative_design_service import CreativeDesignService
class _DummyService:
def __init__(self):
self.user_id = 1
self.cache_dirs = {"style_genes": tmp_path}
self._cached_style_genes = {}
self._cached_global_constitutions = {}
self._cached_page_creative_briefs = {}
self._cached_slide_creative_guides = {}
self._style_genes_ready_events = {}
self._global_constitution_ready_events = {}
self._page_creative_brief_ready_events = {}
self._slide_creative_guide_ready_events = {}
async def _get_user_generation_config(self):
return {"enable_per_slide_creative_guidance": False}
owner = _DummyService()
service = CreativeDesignService(owner)
async def fake_style_genes(project_id, template_html, page_number):
return "STYLE-GENES"
async def fake_constitution(*args, **kwargs):
return "GLOBAL-CONSTITUTION"
async def fake_page_briefs(*args, **kwargs):
return [
{"page": 1, "creative_brief": "封面指导"},
{"page": 2, "creative_brief": "PAGE-BRIEF"},
]
async def fail_slide_guide(*args, **kwargs):
raise AssertionError("slide-level prompt should not be used when per-slide guidance is disabled")
service._get_or_extract_style_genes = fake_style_genes
service._get_or_generate_global_constitution = fake_constitution
service._get_or_generate_page_creative_briefs = fake_page_briefs
service._get_or_generate_slide_creative_guide = fail_slide_guide
result = asyncio.run(
service._get_creative_design_inputs(
project_id="proj-1",
template_html="<div></div>",
slide_data={"title": "增长机会"},
page_number=2,
total_pages=5,
confirmed_requirements={"topic": "年度复盘"},
all_slides=[{"title": "封面"}, {"title": "增长机会"}],
)
)
assert result == ("STYLE-GENES", "GLOBAL-CONSTITUTION", "PAGE-BRIEF")
def test_creative_design_service_persists_slide_creative_guide_cache(tmp_path, monkeypatch):
from landppt.services.slide.creative_design_service import CreativeDesignService
class _DummyService:
def __init__(self):
self.user_id = 1
self.cache_dirs = {"style_genes": tmp_path}
self._cached_slide_creative_guides = {}
self._slide_creative_guide_ready_events = {}
owner = _DummyService()
service = CreativeDesignService(owner)
recorded = []
async def fake_generate(*args, **kwargs):
recorded.append((args, kwargs))
return "REALTIME-GUIDE"
monkeypatch.setattr(service, "_generate_slide_creative_guide", fake_generate)
guide = asyncio.run(
service._get_or_generate_slide_creative_guide(
project_id="proj-cache",
slide_data={"title": "当前页"},
page_number=3,
total_pages=8,
confirmed_requirements={"topic": "项目汇报"},
all_slides=[{"title": "封面"}, {"title": "目录"}, {"title": "当前页"}],
template_html="<div class='page'></div>",
)
)
cache_file = Path(tmp_path) / "proj-cache_slide_3_creative_guide.json"
cache_data = json.loads(cache_file.read_text(encoding="utf-8"))
assert guide == "REALTIME-GUIDE"
assert len(recorded) == 1
assert owner._cached_slide_creative_guides["proj-cache:3"] == "REALTIME-GUIDE"
assert cache_data["creative_guide"] == "REALTIME-GUIDE"
def test_creative_guidance_runtime_pipeline_wires_per_slide_prompt_and_cache_cleanup():
root = Path(__file__).resolve().parents[1]
service_text = (root / "src/landppt/services/slide/creative_design_service.py").read_text(encoding="utf-8")
assert 'enable_per_slide_creative_guidance' in service_text
assert 'get_slide_design_guide_prompt(' in service_text
assert '_get_or_generate_slide_creative_guide(' in service_text
assert '_cached_slide_creative_guides' in service_text
assert '_slide_creative_guide_ready_events' in service_text
assert '_slide_*_creative_guide.json' in service_text