allow selection between portrait, landscape, square canvas types

This commit is contained in:
2026-08-17 12:19:31 -04:00
parent 5518f07234
commit a73a10fdb6
4 changed files with 247 additions and 4 deletions
+156
View File
@@ -961,3 +961,159 @@ def test_debug_tools(mock_ctx: MagicMock) -> None:
assert "LLM Tools" in call_args
assert "get_channel_members" in call_args
assert "members" in call_args.lower()
# ---------------------------------------------------------------------------
# Image layout selection (doodlebob)
# ---------------------------------------------------------------------------
LAYOUT_TEST_SIZES = {
"portrait": "1024x1536",
"landscape": "1536x1024",
"square": "1024x1024",
}
def _sent_texts(mock_ctx: MagicMock) -> list[str]:
"""Collect positional (text) arguments sent via ctx.send."""
return [c.args[0] for c in mock_ctx.send.call_args_list if c.args]
@pytest.mark.parametrize(
("response", "expected"),
[
("portrait", "portrait"),
("landscape", "landscape"),
("square", "square"),
(" Portrait ", "portrait"),
("LANDSCAPE", "landscape"),
("square.", "square"),
("I would use portrait.", "portrait"),
("This scene is best as landscape.", "landscape"),
("A square composition works here.", "square"),
],
)
def test_parse_image_layout_valid(response: str, expected: str) -> None:
"""Test that valid LLM layout responses parse to the right layout."""
import vibe_bot.main as main_module
assert main_module.parse_image_layout(response) == expected
@pytest.mark.parametrize(
"response",
[
"",
" ",
"banana",
"1024x1024",
"tall and wide",
"squareness", # word boundary should prevent a match on "square"
],
)
def test_parse_image_layout_defaults_to_square(response: str) -> None:
"""Test that empty or malformed responses fall back to square."""
import vibe_bot.main as main_module
assert main_module.parse_image_layout(response) == "square"
def test_doodlebob_selects_portrait(
mock_ctx: MagicMock,
mock_llama_wrapper: MagicMock,
mock_base64: MagicMock,
) -> None:
"""Test doodlebob picks portrait and passes the portrait size."""
import asyncio
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"portrait", # layout selection
"a tall portrait of a lighthouse", # prompt rewrite
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
with patch.object(main_module, "LAYOUT_SIZES", LAYOUT_TEST_SIZES):
asyncio.run(main_module.doodlebob(mock_ctx, message="a tall lighthouse"))
# Layout selection is the first instruct call, using the layout prompt.
layout_call = mock_llama_wrapper.chat_completion_instruct.call_args_list[0]
assert layout_call.kwargs["system_prompt"] == main_module.IMAGE_LAYOUT_SYSTEM_PROMPT
assert layout_call.kwargs["user_prompt"] == "a tall lighthouse"
mock_llama_wrapper.image_generation.assert_called_once()
assert mock_llama_wrapper.image_generation.call_args.kwargs["size"] == "1024x1536"
sent = _sent_texts(mock_ctx)
assert any("shopping for a canvas" in m for m in sent)
assert any("selected portrait" in m for m in sent)
assert any("drone strike" in m for m in sent)
def test_doodlebob_selects_landscape(
mock_ctx: MagicMock,
mock_llama_wrapper: MagicMock,
mock_base64: MagicMock,
) -> None:
"""Test doodlebob picks landscape and passes the landscape size."""
import asyncio
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"landscape",
"a wide panoramic coastline",
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
with patch.object(main_module, "LAYOUT_SIZES", LAYOUT_TEST_SIZES):
asyncio.run(main_module.doodlebob(mock_ctx, message="wide coastline"))
assert mock_llama_wrapper.image_generation.call_args.kwargs["size"] == "1536x1024"
def test_doodlebob_malformed_layout_defaults_square(
mock_ctx: MagicMock,
mock_llama_wrapper: MagicMock,
mock_base64: MagicMock,
) -> None:
"""Test a malformed layout response falls back to the square size."""
import asyncio
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"banana", # malformed layout
"a balanced composition",
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
with patch.object(main_module, "LAYOUT_SIZES", LAYOUT_TEST_SIZES):
asyncio.run(main_module.doodlebob(mock_ctx, message="a logo"))
assert mock_llama_wrapper.image_generation.call_args.kwargs["size"] == "1024x1024"
sent = _sent_texts(mock_ctx)
assert any("selected square" in m for m in sent)
def test_doodlebob_empty_layout_defaults_square(
mock_ctx: MagicMock,
mock_llama_wrapper: MagicMock,
mock_base64: MagicMock,
) -> None:
"""Test an empty layout response (LLM failure) falls back to square."""
import asyncio
import vibe_bot.main as main_module
mock_llama_wrapper.chat_completion_instruct.side_effect = [
"", # empty layout response
"a balanced composition",
]
mock_llama_wrapper.image_generation.return_value = "aW1hZ2U="
with patch.object(main_module, "LAYOUT_SIZES", LAYOUT_TEST_SIZES):
asyncio.run(main_module.doodlebob(mock_ctx, message="a logo"))
assert mock_llama_wrapper.image_generation.call_args.kwargs["size"] == "1024x1024"