Files
2026-07-13 13:08:41 +08:00

1018 lines
35 KiB
Python

import json
import logging
from types import SimpleNamespace
import pytest
from llmai.shared import (
AssistantMessage,
AssistantToolCall,
GoogleClientConfig,
ImageContentPart,
SystemMessage,
ToolResponseMessage,
UserMessage,
)
from pydantic import BaseModel, Field, ValidationError
from templates.v2.generation import (
CLUSTER_SIMILAR_COMPONENTS_SYSTEM_PROMPT,
CONTENT_ICON_PLACEHOLDER_URL,
CONTENT_IMAGE_PLACEHOLDER_URL,
GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT,
_generate_preview_candidate,
_messages_for_json_repair_retry,
_messages_for_model_validation_retry,
_slide_image_content,
_validate_similarity_groups,
generate_slide_layout,
generate_template,
merge_similar_components,
)
from templates.v2.models.elements import Image as TemplateImage
from templates.v2.models.layouts import (
RawSlideLayout,
RawSlideLayouts,
SimilarComponents,
SimilarComponentsList,
SlideLayout,
SlideLayouts,
)
from templates.v2.tools import PreviewSlideTool
class _FakeResponse:
def __init__(self, content, messages=None, tool_calls=None):
self.content = content
self.messages = messages or []
self.tool_calls = tool_calls or []
class _FakeClient:
def __init__(self, content=None, responses=None):
self.content = content
self.responses = list(responses or [])
self.calls = []
def generate(self, **kwargs):
self.calls.append(kwargs)
if self.responses:
return self.responses.pop(0)
return _FakeResponse(self.content)
class _ProviderResponseItem:
id = "rs_00000000000000000000000000000000"
class _RetrySchema(BaseModel):
title: str = Field(min_length=5)
def _raw_layout(layout_id: str = "source_slide") -> RawSlideLayout:
return RawSlideLayout.model_validate(
{
"id": layout_id,
"description": "Source slide with a title block.",
"elements": [
{
"type": "text",
"position": {"x": 100, "y": 80},
"size": {"width": 600, "height": 80},
"decorative": False,
"name": "title",
"min_length": 20,
"max_length": 40,
"runs": [{"text": "Original title"}],
}
],
}
)
def _generated_layout(layout_id: str = "title_slide") -> dict:
return {
"id": layout_id,
"description": "Reusable slide with a prominent title block.",
"components": [
{
"id": "title_block",
"description": "Reusable prominent title text block.",
"position": {"x": 100, "y": 80},
"size": {"width": 600, "height": 80},
"elements": [
{
"type": "text",
"position": {"x": 0, "y": 0},
"size": {"width": 600, "height": 80},
"decorative": False,
"name": "title",
"min_length": 20,
"max_length": 40,
"runs": [{"text": "Original title"}],
}
],
}
],
}
def _generated_layout_with_images() -> dict:
layout = _generated_layout("image_slide")
layout["components"][0]["elements"] = [
{
"type": "image",
"position": {"x": 0, "y": 0},
"size": {"width": 320, "height": 180},
"decorative": False,
"name": "hero_image",
"data": "/app_data/images/source-photo.png",
"prompt": "Team reviewing dashboard",
"is_icon": False,
},
{
"type": "image",
"position": {"x": 340, "y": 0},
"size": {"width": 48, "height": 48},
"decorative": False,
"name": "status_icon",
"data": "/app_data/icons/source-icon.svg",
"prompt": "growth chart",
"is_icon": True,
},
{
"type": "image",
"position": {"x": 400, "y": 0},
"size": {"width": 80, "height": 40},
"decorative": True,
"name": "logo",
"data": "/app_data/images/logo.png",
"is_icon": False,
},
{
"type": "group",
"position": {"x": 0, "y": 220},
"size": {"width": 180, "height": 120},
"name": "nested_media",
"children": [
{
"type": "image",
"position": {"x": 0, "y": 0},
"size": {"width": 180, "height": 120},
"decorative": False,
"name": "nested_image",
"data": "/app_data/images/nested-photo.png",
"is_icon": False,
}
],
},
]
return layout
def _contains_key(value, key: str) -> bool:
if isinstance(value, dict):
return key in value or any(
_contains_key(child, key) for child in value.values()
)
if isinstance(value, list):
return any(_contains_key(item, key) for item in value)
return False
def test_template_image_supports_optional_overlay_color():
image = TemplateImage.model_validate(
{
"type": "image",
"data": "/app_data/image.png",
"color": "rgba(0, 0, 0, 0.35)",
"decorative": True,
"name": "background",
"is_icon": False,
}
)
image_without_overlay = TemplateImage.model_validate(
{
"type": "image",
"data": "/app_data/image.png",
"decorative": True,
"name": "background",
"is_icon": False,
}
)
assert image.color == "rgba(0, 0, 0, 0.35)"
assert image_without_overlay.color is None
def test_generate_slide_layout_requests_complete_layout(monkeypatch, caplog):
preview_tool_call = AssistantToolCall(
id="preview-call-1",
name="previewSlide",
arguments=json.dumps(_generated_layout()),
)
client = _FakeClient(
responses=[
_FakeResponse(None, tool_calls=[preview_tool_call]),
_FakeResponse(_generated_layout()),
]
)
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
monkeypatch.setattr(
PreviewSlideTool,
"render",
lambda _self, _layout: ImageContentPart(
data=b"rendered-preview",
mime_type="image/png",
),
)
caplog.set_level(logging.INFO, logger="templates.v2.generation")
result = generate_slide_layout(
_raw_layout(),
2,
"https://example.com/slide-3.png",
)
assert result == SlideLayout.model_validate(_generated_layout())
result_element = result.model_dump(mode="json")["components"][0]["elements"][0]
assert result_element["decorative"] is False
assert "fixed" not in result_element
assert len(client.calls) == 2
preview_call = client.calls[0]
assert isinstance(preview_call["tools"][0], PreviewSlideTool)
assert preview_call["tools"][0].input_schema is SlideLayout
assert preview_call["tool_choice"] == {
"mode": "auto",
"tools": ["previewSlide"],
}
assert preview_call["response_format"].json_schema is SlideLayout
assert preview_call["response_format"].name == "SlideLayoutResponse"
assert "max_tokens" not in preview_call
assert preview_call["messages"][0].content == GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
user_content = preview_call["messages"][1].content
assert user_content[0].url == "https://example.com/slide-3.png"
payload = json.loads(user_content[1])
assert payload[0]["id"] == "source_slide"
assert payload[0]["elements"][0]["runs"][0]["text"] == (
"Original title"
)
assert not _contains_key(payload, "decorative")
final_call = client.calls[1]
assert final_call["response_format"].json_schema is SlideLayout
assert final_call["response_format"].name == "SlideLayoutResponse"
assert "max_tokens" not in final_call
assert isinstance(final_call["messages"][-2], ToolResponseMessage)
feedback = final_call["messages"][-1]
assert isinstance(feedback, UserMessage)
assert feedback.content[0].data == b"rendered-preview"
assert "Review this rendered candidate" in feedback.content[1]
messages = [record.getMessage() for record in caplog.records]
assert any("slide 3: preview slide called" in message for message in messages)
assert any("slide 3: preview slide rendered" in message for message in messages)
assert any("slide 3: slide layout JSON returned" in message for message in messages)
def test_generate_slide_layout_accepts_direct_schema_response(monkeypatch, caplog):
client = _FakeClient(responses=[_FakeResponse(_generated_layout())])
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
monkeypatch.setattr(
PreviewSlideTool,
"render",
lambda _self, _layout: pytest.fail("preview should not be rendered"),
)
caplog.set_level(logging.INFO, logger="templates.v2.generation")
result = generate_slide_layout(
_raw_layout(),
0,
"https://example.com/slide-1.png",
)
assert result == SlideLayout.model_validate(_generated_layout())
assert len(client.calls) == 1
call = client.calls[0]
assert call["tool_choice"] == {
"mode": "auto",
"tools": ["previewSlide"],
}
assert call["response_format"].json_schema is SlideLayout
assert call["response_format"].name == "SlideLayoutResponse"
messages = [record.getMessage() for record in caplog.records]
assert any("slide 1: slide layout JSON returned" in message for message in messages)
def test_generate_slide_layout_replaces_content_image_urls(monkeypatch):
client = _FakeClient(responses=[_FakeResponse(_generated_layout_with_images())])
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
monkeypatch.setattr(
PreviewSlideTool,
"render",
lambda _self, _layout: pytest.fail("preview should not be rendered"),
)
result = generate_slide_layout(
_raw_layout(),
0,
"https://example.com/slide-1.png",
)
elements = result.model_dump(mode="json")["components"][0]["elements"]
assert elements[0]["data"] == CONTENT_IMAGE_PLACEHOLDER_URL
assert elements[0]["prompt"] == "Team reviewing dashboard"
assert elements[1]["data"] == CONTENT_ICON_PLACEHOLDER_URL
assert elements[1]["prompt"] == "growth chart"
assert elements[2]["data"] == "/app_data/images/logo.png"
assert elements[3]["children"][0]["data"] == CONTENT_IMAGE_PLACEHOLDER_URL
def test_generate_slide_layout_passes_max_tokens_when_provided(monkeypatch):
client = _FakeClient(responses=[_FakeResponse(_generated_layout())])
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
monkeypatch.setattr(
PreviewSlideTool,
"render",
lambda _self, _layout: pytest.fail("preview should not be rendered"),
)
result = generate_slide_layout(
_raw_layout(),
0,
"https://example.com/slide-1.png",
max_tokens=16000,
)
assert result == SlideLayout.model_validate(_generated_layout())
assert client.calls[0]["max_tokens"] == 16000
def test_generate_slide_layout_uses_json_schema_response_for_google(monkeypatch):
client = _FakeClient(responses=[_FakeResponse(_generated_layout())])
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr(
"templates.v2.generation.get_llm_config",
lambda: GoogleClientConfig(api_key="test-key"),
)
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "gemini-test")
monkeypatch.setattr(
PreviewSlideTool,
"render",
lambda _self, _layout: pytest.fail("preview should not be rendered"),
)
result = generate_slide_layout(
_raw_layout(),
0,
"https://example.com/slide-1.png",
)
assert result == SlideLayout.model_validate(_generated_layout())
call = client.calls[0]
assert call["response_format"].json_schema is SlideLayout
assert call["response_format"].name == "SlideLayoutResponse"
assert call["messages"][0].content == GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
def test_generate_preview_candidate_returns_last_preview_tool_json(monkeypatch, caplog):
preview_tool_call = AssistantToolCall(
id="preview-call-1",
name="previewSlide",
arguments=json.dumps(_generated_layout()),
)
client = _FakeClient(
responses=[_FakeResponse(None, tool_calls=[preview_tool_call])]
)
render_calls = []
def fake_render(_self, layout):
render_calls.append(layout.id)
return ImageContentPart(
data=b"rendered-preview",
mime_type="image/png",
)
monkeypatch.setattr(PreviewSlideTool, "render", fake_render)
caplog.set_level(logging.INFO, logger="templates.v2.generation")
result = _generate_preview_candidate(
client=client,
model="test-model",
messages=[
SystemMessage(content=GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT),
UserMessage(content="{}"),
],
label="slide layout",
preview_tool=PreviewSlideTool(),
validation_retries=0,
)
assert result == SlideLayout.model_validate(_generated_layout())
assert render_calls == ["title_slide"]
assert len(client.calls) == 1
call = client.calls[0]
assert call["response_format"].json_schema is SlideLayout
assert "max_tokens" not in call
messages = [record.getMessage() for record in caplog.records]
assert any(
"slide layout: preview slide rendered" in message
for message in messages
)
assert any(
"slide layout: returning preview slide JSON as final" in message
for message in messages
)
def test_generate_preview_candidate_preserves_provider_response_messages(monkeypatch):
preview_tool_call = AssistantToolCall(
id="preview-call-1",
name="previewSlide",
arguments=json.dumps(_generated_layout("first_candidate")),
)
preserved_assistant_message = AssistantMessage(
content=["provider-preserved-context"],
tool_calls=[preview_tool_call],
)
initial_messages = [
SystemMessage(content=GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT),
UserMessage(content="{}"),
]
client = _FakeClient(
responses=[
_FakeResponse(
None,
messages=[*initial_messages, preserved_assistant_message],
tool_calls=[preview_tool_call],
),
_FakeResponse(_generated_layout("final_candidate")),
]
)
monkeypatch.setattr(
PreviewSlideTool,
"render",
lambda _self, _layout: ImageContentPart(
data=b"rendered-preview",
mime_type="image/png",
),
)
result = _generate_preview_candidate(
client=client,
model="test-model",
messages=initial_messages,
label="slide layout",
preview_tool=PreviewSlideTool(),
validation_retries=1,
)
assert result == SlideLayout.model_validate(_generated_layout("final_candidate"))
follow_up_messages = client.calls[1]["messages"]
assert follow_up_messages[2] is preserved_assistant_message
assert follow_up_messages[3].id == "preview-call-1"
assert follow_up_messages[4].content[0].data == b"rendered-preview"
assert "Original slide image:" not in follow_up_messages[4].content
def test_generate_slide_layout_allows_second_preview_then_returns_final_json(
monkeypatch,
caplog,
):
first_preview_tool_call = AssistantToolCall(
id="preview-call-1",
name="previewSlide",
arguments=json.dumps(_generated_layout("first_candidate")),
)
second_preview_tool_call = AssistantToolCall(
id="preview-call-2",
name="previewSlide",
arguments=json.dumps(_generated_layout("second_candidate")),
)
client = _FakeClient(
responses=[
_FakeResponse(None, tool_calls=[first_preview_tool_call]),
_FakeResponse(None, tool_calls=[second_preview_tool_call]),
_FakeResponse(_generated_layout("final_candidate")),
]
)
render_calls = []
def fake_render(_self, layout):
render_calls.append(layout.id)
return ImageContentPart(
data=b"rendered-preview",
mime_type="image/png",
)
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
monkeypatch.setattr(PreviewSlideTool, "render", fake_render)
caplog.set_level(logging.INFO, logger="templates.v2.generation")
result = generate_slide_layout(
_raw_layout(),
0,
"https://example.com/slide-1.png",
)
assert result == SlideLayout.model_validate(_generated_layout("final_candidate"))
assert render_calls == ["first_candidate", "second_candidate"]
assert len(client.calls) == 3
second_call = client.calls[1]
assert isinstance(second_call["messages"][-2], ToolResponseMessage)
assert second_call["messages"][-2].id == "preview-call-1"
second_feedback = second_call["messages"][-1]
assert isinstance(second_feedback, UserMessage)
assert second_feedback.content[0].data == b"rendered-preview"
assert "one more time" in second_feedback.content[1]
third_call = client.calls[2]
assert "tools" not in third_call
assert "tool_choice" not in third_call
assert isinstance(third_call["messages"][-2], ToolResponseMessage)
assert third_call["messages"][-2].id == "preview-call-2"
final_feedback = third_call["messages"][-1]
assert isinstance(final_feedback, UserMessage)
assert final_feedback.content[0].data == b"rendered-preview"
assert "maximum number of previewSlide calls" in final_feedback.content[1]
messages = [record.getMessage() for record in caplog.records]
assert any("slide 1: preview slide called" in message for message in messages)
assert any("preview_call=2" in message for message in messages)
assert any(
"slide 1: slide layout JSON returned" in message
for message in messages
)
def test_generate_template_generates_each_slide_and_preserves_order(monkeypatch):
raw_layouts = RawSlideLayouts(
layouts=[_raw_layout("first"), _raw_layout("second")]
)
calls = []
def fake_generate(source_layout, slide_index, slide_image_url, fonts=None):
calls.append((source_layout.id, slide_index, slide_image_url, fonts))
return SlideLayout.model_validate(
_generated_layout(f"generated_{source_layout.id}")
)
monkeypatch.setattr(
"templates.v2.generation.generate_slide_layout", fake_generate
)
generated = generate_template(
raw_layouts,
["https://example.com/first.png", "https://example.com/second.png"],
{"Inter": "https://example.com/inter.css"},
)
assert sorted(calls) == [
(
"first",
0,
"https://example.com/first.png",
{"Inter": "https://example.com/inter.css"},
),
(
"second",
1,
"https://example.com/second.png",
{"Inter": "https://example.com/inter.css"},
),
]
assert [layout.id for layout in generated.layouts] == [
"generated_first",
"generated_second",
]
def test_generate_template_repairs_duplicate_generated_layout_ids(monkeypatch):
raw_layouts = RawSlideLayouts(
layouts=[_raw_layout("first"), _raw_layout("second")]
)
def fake_generate(source_layout, slide_index, slide_image_url, fonts=None):
return SlideLayout.model_validate(_generated_layout("duplicate_layout"))
monkeypatch.setattr(
"templates.v2.generation.generate_slide_layout", fake_generate
)
generated = generate_template(
raw_layouts,
["https://example.com/first.png", "https://example.com/second.png"],
)
assert [layout.id for layout in generated.layouts] == [
"duplicate_layout",
"duplicate_layout_2",
]
def test_generate_template_rejects_empty_source():
with pytest.raises(ValueError, match="at least one"):
generate_template(RawSlideLayouts(layouts=[]), [])
def test_generate_template_requires_one_image_per_layout():
with pytest.raises(ValueError, match="one image for each layout"):
generate_template(
RawSlideLayouts(layouts=[_raw_layout("first"), _raw_layout("second")]),
["https://example.com/first.png"],
)
def test_merge_similar_components_clusters_by_global_component_index(
monkeypatch, caplog
):
first = _generated_layout("first_layout")
first["components"][0]["id"] = "title_block"
first["components"][0]["description"] = (
"Reusable prominent title text block for opening slides."
)
second = _generated_layout("second_layout")
second["components"][0]["id"] = "metric_grid"
second["components"][0]["description"] = (
"Reusable grid presenting several business metrics and labels."
)
second["components"][0]["size"] = {"width": 600, "height": 180}
second["components"][0]["elements"] = [
{
"type": "grid",
"position": {"x": 0, "y": 0},
"size": {"width": 600, "height": 180},
"columns": 2,
"rows": 1,
"gap": 24,
"name": "metrics",
"min_children": 1,
"max_children": 2,
"children": [
{
"type": "text",
"size": {"width": 280, "height": 80},
"decorative": False,
"name": "metric_value",
"min_length": 1,
"max_length": 10,
"runs": [{"text": "42%"}],
},
{
"type": "text",
"size": {"width": 280, "height": 80},
"decorative": False,
"name": "metric_label",
"min_length": 5,
"max_length": 30,
"runs": [{"text": "Revenue growth"}],
},
],
}
]
third = _generated_layout("third_layout")
third["components"][0]["id"] = "section_heading"
third["components"][0]["description"] = (
"Reusable prominent heading text block for section slides."
)
layouts = SlideLayouts.model_validate({"layouts": [first, second, third]})
client = _FakeClient(
{
"similar_components": [
{"indices": [0, 2]},
]
}
)
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
caplog.set_level(logging.INFO, logger="templates.v2.generation")
merged = merge_similar_components(layouts)
assert len(merged.components) == 2
assert merged.components[0].id == "title_block"
assert [variant.id for variant in merged.components[0].variants] == [
"title_block",
"section_heading",
]
assert [variant.id for variant in merged.components[1].variants] == [
"metric_grid"
]
call = client.calls[0]
assert call["response_format"].json_schema.__name__ == "SimilarComponentsList"
assert call["response_format"].name == "SimilarComponentsResponse"
assert call["messages"][0].content == CLUSTER_SIMILAR_COMPONENTS_SYSTEM_PROMPT
payload = json.loads(call["messages"][1].content)
assert payload == {
"components": [
{
"index": 0,
"id": "title_block",
"description": (
"Reusable prominent title text block for opening slides."
),
},
{
"index": 1,
"id": "metric_grid",
"description": (
"Reusable grid presenting several business metrics and labels."
),
},
{
"index": 2,
"id": "section_heading",
"description": (
"Reusable prominent heading text block for section slides."
),
},
]
}
messages = "\n".join(record.getMessage() for record in caplog.records)
assert "similar_components" not in messages
assert "schema=SimilarComponentsResponse" in messages
def test_merge_similar_components_skips_llm_for_single_component(monkeypatch):
monkeypatch.setattr(
"templates.v2.generation.get_client",
lambda **_kwargs: pytest.fail("LLM should not be called"),
)
layouts = SlideLayouts.model_validate({"layouts": [_generated_layout()]})
merged = merge_similar_components(layouts)
assert len(merged.components) == 1
assert merged.components[0].id == "title_block"
assert len(merged.components[0].variants) == 1
def test_merge_similar_components_removes_structural_duplicates_after_clustering(
monkeypatch,
):
first = _generated_layout("first_layout")
first["components"][0]["id"] = "headline_a"
first["components"][0]["description"] = (
"Reusable headline card with static divider decoration."
)
first["components"][0]["elements"] = [
{
"type": "rectangle",
"position": {"x": 0, "y": 70},
"size": {"width": 600, "height": 4},
"fill": {"color": "#111111"},
},
{
"type": "text",
"position": {"x": 0, "y": 0},
"size": {"width": 600, "height": 60},
"decorative": False,
"name": "headline",
"min_length": 5,
"max_length": 60,
"runs": [{"text": "First headline content"}],
},
]
second = _generated_layout("second_layout")
second["components"][0]["id"] = "headline_b"
second["components"][0]["description"] = (
"Reusable title card with the same static divider decoration."
)
second["components"][0]["position"] = {"x": 260, "y": 180}
second["components"][0]["elements"] = [
{
"type": "rectangle",
"position": {"x": 0, "y": 70},
"size": {"width": 600, "height": 4},
"fill": {"color": "#111111"},
},
{
"type": "text",
"position": {"x": 0, "y": 0},
"size": {"width": 600, "height": 60},
"decorative": False,
"name": "title",
"min_length": 5,
"max_length": 80,
"runs": [{"text": "Different editable title copy"}],
},
]
third = _generated_layout("third_layout")
third["components"][0]["id"] = "headline_c"
third["components"][0]["description"] = (
"Reusable headline card with a different static divider decoration."
)
third["components"][0]["elements"] = [
{
"type": "rectangle",
"position": {"x": 0, "y": 70},
"size": {"width": 600, "height": 4},
"fill": {"color": "#DDDDDD"},
},
{
"type": "text",
"position": {"x": 0, "y": 0},
"size": {"width": 600, "height": 60},
"decorative": False,
"name": "headline",
"min_length": 5,
"max_length": 60,
"runs": [{"text": "Third headline content"}],
},
]
layouts = SlideLayouts.model_validate({"layouts": [first, second, third]})
client = _FakeClient({"similar_components": []})
monkeypatch.setattr("templates.v2.generation.get_client", lambda **_kwargs: client)
monkeypatch.setattr("templates.v2.generation.get_llm_config", lambda: {})
monkeypatch.setattr("templates.v2.generation.get_model", lambda: "test-model")
merged = merge_similar_components(layouts)
assert len(client.calls) == 1
assert len(merged.components) == 2
assert [variant.id for variant in merged.components[0].variants] == [
"headline_a",
"headline_b",
]
assert [variant.id for variant in merged.components[1].variants] == [
"headline_c",
]
def test_similar_components_requires_unique_non_negative_indices():
with pytest.raises(ValidationError, match="must be unique"):
SimilarComponents(indices=[1, 1])
with pytest.raises(ValidationError, match="non-negative"):
SimilarComponents(indices=[-1, 1])
def test_similarity_groups_reject_overlapping_and_out_of_range_indices():
overlapping = SimilarComponentsList.model_validate(
{
"similar_components": [
{"indices": [0, 1]},
{"indices": [1, 2]},
]
}
)
with pytest.raises(ValueError, match="more than one"):
_validate_similarity_groups(overlapping, component_count=3)
out_of_range = SimilarComponentsList.model_validate(
{"similar_components": [{"indices": [0, 3]}]}
)
with pytest.raises(ValueError, match="outside the available range"):
_validate_similarity_groups(out_of_range, component_count=3)
def test_slide_image_content_embeds_local_image_bytes(tmp_path, monkeypatch):
image_path = tmp_path / "slide.png"
image_path.write_bytes(b"png-image-bytes")
monkeypatch.setattr(
"templates.v2.generation.resolve_image_path_to_filesystem",
lambda _url: str(image_path),
)
image_content = _slide_image_content("/app_data/images/slide.png")
assert image_content.data == b"png-image-bytes"
assert image_content.mime_type == "image/png"
assert image_content.url is None
def test_preview_slide_tool_renders_layout_components(tmp_path, monkeypatch):
app_data_dir = tmp_path / "app-data"
preview_path = tmp_path / "preview.png"
preview_path.write_bytes(b"rendered-slide")
captured = {}
async def fake_render_json_to_image(data, width, height, fonts=None):
captured["data"] = data
captured["width"] = width
captured["height"] = height
captured["fonts"] = fonts
return SimpleNamespace(path=str(preview_path))
monkeypatch.setattr(
"templates.v2.tools.EXPORT_TASK_SERVICE.render_json_to_image",
fake_render_json_to_image,
)
monkeypatch.setenv("APP_DATA_DIRECTORY", str(app_data_dir))
image = PreviewSlideTool(
slide_index=2,
fonts={"Inter": "https://example.com/inter.css"},
).render(
SlideLayout.model_validate(_generated_layout())
)
saved_json_path = app_data_dir / "preview_slide" / "2" / "1.json"
saved_image_path = app_data_dir / "preview_slide" / "2" / "1.png"
assert captured["data"][0]["id"] == "title_block"
assert captured["data"][0]["elements"][0]["type"] == "text"
assert captured["width"] == 1280
assert captured["height"] == 720
assert captured["fonts"] == {"Inter": "https://example.com/inter.css"}
assert image.data == b"rendered-slide"
assert image.mime_type == "image/png"
assert json.loads(saved_json_path.read_text()) == _generated_layout()
assert saved_image_path.read_bytes() == b"rendered-slide"
def test_slide_layout_rejects_duplicate_component_ids():
layout = _generated_layout()
layout["components"].append(layout["components"][0])
with pytest.raises(ValidationError, match="component ids must be unique"):
SlideLayout.model_validate(layout)
def test_slide_layout_does_not_accept_fixed_component_metadata():
layout = _generated_layout()
element = layout["components"][0]["elements"][0]
element["fixed"] = element.pop("decorative")
with pytest.raises(ValidationError):
SlideLayout.model_validate(layout)
def test_direct_generation_prompt_uses_decorative_element_metadata():
assert "Convert the provided raw slide elements to components" in (
GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
)
assert "# Decorative and Content Element Rules:" in (
GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
)
assert "`decorative=true`" in GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
assert "`decorative=false`" in GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
assert "If removal changes meaning, it is content" in (
GENERATE_SLIDE_LAYOUT_SYSTEM_PROMPT
)
def test_json_repair_retry_rebuilds_messages_without_provider_response_items():
original_messages = [
SystemMessage(content="Return JSON."),
UserMessage(content="{}"),
]
provider_response_item = _ProviderResponseItem()
response = _FakeResponse(
content='{"bad": true',
messages=[provider_response_item],
)
retry_messages = _messages_for_json_repair_retry(
messages=original_messages,
response=response,
label="slide layout",
error=ValueError("invalid JSON"),
)
assert provider_response_item not in retry_messages
assert retry_messages[:2] == original_messages
assert isinstance(retry_messages[2], AssistantMessage)
assert retry_messages[2].content == ['"{\\"bad\\": true"']
assert isinstance(retry_messages[3], UserMessage)
assert "Return a complete replacement JSON object." in retry_messages[3].content
def test_validation_retry_rebuilds_messages_without_provider_response_items():
original_messages = [
SystemMessage(content="Return schema JSON."),
UserMessage(content='{"title":"ok"}'),
]
provider_response_item = _ProviderResponseItem()
invalid_response = {"title": "bad"}
response = _FakeResponse(
content=invalid_response,
messages=[provider_response_item],
)
with pytest.raises(ValidationError) as exc:
_RetrySchema.model_validate(invalid_response)
retry_messages = _messages_for_model_validation_retry(
messages=original_messages,
response=response,
label="slide layout",
output_model=_RetrySchema,
error=exc.value,
invalid_response=invalid_response,
)
assert provider_response_item not in retry_messages
assert retry_messages[:2] == original_messages
assert isinstance(retry_messages[2], AssistantMessage)
assert retry_messages[2].content == ['{\n "title": "bad"\n}']
assert isinstance(retry_messages[3], UserMessage)
assert "required_json_schema:" in retry_messages[3].content