The pack cited internal planning-doc items (plan P3-7, decision D1, plan risk 9.2) that aren't public, leaving outside contributors with pointers they can't follow. Each comment already states its reason; only the dangling citation is removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
328 lines
10 KiB
Python
328 lines
10 KiB
Python
"""
|
|
Facet-reading tests, against a fixture captured from the live API
|
|
(GET /api/v1/styles/{ref}?format=json) so the section and field names under test
|
|
are the real ones, not invented.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from styleref_style import (
|
|
COMPOSITION_SECTION,
|
|
LIGHTING_SECTION,
|
|
attribution,
|
|
is_style,
|
|
make_style,
|
|
mood_terms,
|
|
negative_terms,
|
|
palette_hexes,
|
|
section_summary,
|
|
summary_line,
|
|
)
|
|
|
|
SECTIONS = {
|
|
"colors": {
|
|
"weight": "Absolute",
|
|
"values": {
|
|
"color_palette": {
|
|
"mode": "solid",
|
|
"gradients": [],
|
|
"solidColors": [
|
|
{"hex": "#F6F6F4", "amount": 35},
|
|
{"hex": "#D7DDE0", "amount": 15},
|
|
{"hex": "#1B2A44", "amount": 10},
|
|
],
|
|
},
|
|
"palette_size": "8 colors",
|
|
},
|
|
},
|
|
"mood_personality": {
|
|
"weight": "Primary",
|
|
"values": {"mood_personality": ["Serious", "Calm", "Minimalist"], "energy_level": "Low"},
|
|
},
|
|
"light_shadow": {
|
|
"weight": "secondary",
|
|
"values": {"light_source_type": "Natural", "shadow_type": "Soft"},
|
|
},
|
|
"spatial_hierarchy": {
|
|
"weight": "secondary",
|
|
"values": {"perspective_type": "Eye level", "symmetry": "Asymmetric"},
|
|
},
|
|
"guardrails": {
|
|
"weight": "absolute",
|
|
"values": {"avoided_visuals": "Lens flares; bokeh; film grain"},
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def style():
|
|
return make_style(
|
|
ref="9a2adtz6-cd8d77ee2f51",
|
|
name="Real Moments",
|
|
sections=SECTIONS,
|
|
author="@styleref",
|
|
url="https://styleref.io/share/9a2adtz6-cd8d77ee2f51",
|
|
)
|
|
|
|
|
|
def test_palette_preserves_declared_order(style):
|
|
"""Order matters — it runs most-used first, so [0] is the dominant color."""
|
|
assert palette_hexes(style) == ["#F6F6F4", "#D7DDE0", "#1B2A44"]
|
|
|
|
|
|
def test_palette_includes_gradient_stops():
|
|
"""A gradient-mode palette would otherwise read as having no colors at all."""
|
|
style = make_style(
|
|
ref="x",
|
|
name="Gradient",
|
|
sections={
|
|
"colors": {
|
|
"values": {
|
|
"color_palette": {
|
|
"mode": "gradient",
|
|
"solidColors": [],
|
|
"gradients": [{"stops": [{"hex": "#FF0000"}, {"hex": "#0000FF"}]}],
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
assert palette_hexes(style) == ["#FF0000", "#0000FF"]
|
|
|
|
|
|
def test_palette_dedupes_case_insensitively():
|
|
style = make_style(
|
|
ref="x",
|
|
name="Dupes",
|
|
sections={
|
|
"colors": {
|
|
"values": {
|
|
"color_palette": {
|
|
"solidColors": [{"hex": "#AABBCC"}, {"hex": "#aabbcc"}, {"hex": "#123456"}]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
assert palette_hexes(style) == ["#AABBCC", "#123456"]
|
|
|
|
|
|
def test_palette_is_empty_for_a_style_without_colors():
|
|
assert palette_hexes(make_style(ref="x", name="Bare")) == []
|
|
|
|
|
|
def test_mood_list_flattens_to_a_prompt_fragment(style):
|
|
assert mood_terms(style) == "Serious, Calm, Minimalist"
|
|
|
|
|
|
def test_negatives_are_normalized_comma_terms(style):
|
|
"""
|
|
The negative output is negative-channel idiom: terse comma-separated terms
|
|
(an SD negative box negates literal tokens, so no directive words, and the
|
|
convention there is commas, not semicolons).
|
|
"""
|
|
assert negative_terms(style) == "Lens flares, bokeh, film grain"
|
|
|
|
|
|
def test_negatives_cover_artifacts_and_avoided_colors():
|
|
"""
|
|
Guardrails carry more than avoided_visuals — artifacts to avoid
|
|
and avoided colors are user-authored constraints and belong in the negative.
|
|
"""
|
|
style = make_style(
|
|
ref="x",
|
|
name="Guarded",
|
|
sections={
|
|
"guardrails": {
|
|
"values": {
|
|
"avoided_visuals": "Lens flares",
|
|
"common_artifacts_to_avoid": ["AI Gloss / Plastic Smoothing", "Uncanny skin"],
|
|
"avoided_colors": "neon green",
|
|
}
|
|
}
|
|
},
|
|
)
|
|
negative = negative_terms(style)
|
|
assert "Lens flares" in negative
|
|
assert "AI Gloss" in negative
|
|
assert "neon green" in negative
|
|
|
|
|
|
def test_negatives_strip_directive_words_and_split_sentences():
|
|
"""
|
|
"Avoid harsh shadows" in a negative channel would negate the word "avoid".
|
|
Users author sentences; the channel needs terms.
|
|
"""
|
|
from styleref_style import split_guardrail_terms
|
|
|
|
style = make_style(
|
|
ref="x",
|
|
name="Messy",
|
|
sections={
|
|
"guardrails": {
|
|
"values": {
|
|
"avoided_visuals": "Avoid harsh drop shadows; no neon glow",
|
|
"avoided_colors": "never use hot pink",
|
|
"avoided_layouts": "cluttered grids",
|
|
}
|
|
}
|
|
},
|
|
)
|
|
assert negative_terms(style) == "harsh drop shadows, neon glow, hot pink, cluttered grids"
|
|
# "black and white" is one concept — never split on "and".
|
|
assert split_guardrail_terms("black and white photography") == ["black and white photography"]
|
|
|
|
|
|
def test_negatives_exclude_text_domain_words():
|
|
"""avoided_words constrains text output (buzzwords) — not image negatives."""
|
|
style = make_style(
|
|
ref="x",
|
|
name="Wordy",
|
|
sections={"guardrails": {"values": {"avoided_words": "synergy; disrupt"}}},
|
|
)
|
|
assert negative_terms(style) == ""
|
|
|
|
|
|
def test_negatives_dedupe_repeated_fields():
|
|
style = make_style(
|
|
ref="x",
|
|
name="Dupes",
|
|
sections={
|
|
"guardrails": {
|
|
"values": {
|
|
"avoided_visuals": "film grain",
|
|
"common_artifacts_to_avoid": "Film Grain",
|
|
}
|
|
}
|
|
},
|
|
)
|
|
assert negative_terms(style).lower().count("film grain") == 1
|
|
|
|
|
|
def test_section_summary_is_readable_lines(style):
|
|
summary = section_summary(style, LIGHTING_SECTION)
|
|
assert "light source type: Natural" in summary
|
|
assert "shadow type: Soft" in summary
|
|
|
|
|
|
def test_section_summary_of_missing_section_is_empty(style):
|
|
assert section_summary(style, "does_not_exist") == ""
|
|
|
|
|
|
def test_composition_section_resolves(style):
|
|
assert "perspective type: Eye level" in section_summary(style, COMPOSITION_SECTION)
|
|
|
|
|
|
def test_unknown_sections_do_not_break_reading():
|
|
"""A section added server-side must flow through, not raise."""
|
|
style = make_style(ref="x", name="Future", sections={"brand_new": {"values": {"a": "b"}}})
|
|
assert section_summary(style, "brand_new") == "a: b"
|
|
assert palette_hexes(style) == []
|
|
|
|
|
|
def test_attribution_includes_author_and_url(style):
|
|
assert attribution(style) == "Real Moments by @styleref — https://styleref.io/share/9a2adtz6-cd8d77ee2f51"
|
|
|
|
|
|
def test_attribution_degrades_without_author_or_url():
|
|
assert attribution(make_style(ref="x", name="Solo")) == "Solo"
|
|
|
|
|
|
def test_summary_line_counts_fields(style):
|
|
assert "5 sections" in summary_line(style)
|
|
|
|
|
|
def test_is_style_rejects_arbitrary_dicts():
|
|
"""Guards the node inputs against a mis-wired connection."""
|
|
assert is_style({"foo": "bar"}) is False
|
|
assert is_style("a string") is False
|
|
assert is_style(make_style(ref="x", name="ok")) is True
|
|
|
|
|
|
# ── dimensions (width/height from the style) ─────────────────────────────────
|
|
|
|
|
|
def _fmt_style(aspect: str | None = None, resolution: str | None = None):
|
|
values = {}
|
|
if aspect:
|
|
values["aspect_ratio"] = aspect
|
|
if resolution:
|
|
values["resolution_target"] = resolution
|
|
return make_style(ref="x", name="Fmt", sections={"output_format": {"values": values}})
|
|
|
|
|
|
def test_dimensions_16_9_at_1k():
|
|
from styleref_style import dimensions
|
|
|
|
assert dimensions(_fmt_style("16:9 (Landscape)", "1K")) == (1024, 576)
|
|
|
|
|
|
def test_dimensions_portrait_puts_the_long_edge_on_height():
|
|
from styleref_style import dimensions
|
|
|
|
width, height = dimensions(_fmt_style("9:16 (Portrait)", "2K"))
|
|
assert height == 2048
|
|
assert width == 1152
|
|
|
|
|
|
def test_dimensions_default_to_square_1024():
|
|
from styleref_style import dimensions
|
|
|
|
assert dimensions(make_style(ref="x", name="Bare")) == (1024, 1024)
|
|
# dpi / vector targets aren't pixel sizes — fall back to the default edge.
|
|
assert dimensions(_fmt_style("1:1 (Square)", "300dpi")) == (1024, 1024)
|
|
|
|
|
|
def test_dimensions_are_sampler_friendly_multiples_of_8():
|
|
from styleref_style import dimensions
|
|
|
|
width, height = dimensions(_fmt_style("2.39:1 (Cinematic Landscape)", "1K"))
|
|
assert width % 8 == 0 and height % 8 == 0
|
|
|
|
|
|
# ── custom items & inspiration urls ──────────────────────────────────────────
|
|
|
|
|
|
def test_custom_item_reads_by_index_and_defaults_empty():
|
|
from styleref_style import custom_item
|
|
|
|
style = make_style(
|
|
ref="x",
|
|
name="Custom",
|
|
sections={"custom_style_items": {"values": {"custom_style_item_2": "brand watermark"}}},
|
|
)
|
|
assert custom_item(style, 2) == "brand watermark"
|
|
assert custom_item(style, 1) == ""
|
|
|
|
|
|
def test_inspiration_image_urls_in_stored_order():
|
|
from styleref_style import inspiration_image_urls
|
|
|
|
style = make_style(
|
|
ref="x",
|
|
name="Insp",
|
|
sections={
|
|
"inspiration_images": {
|
|
"values": {
|
|
"inspiration_images": [
|
|
{"url": "https://cdn.styleref.io/a.webp"},
|
|
{"url": "https://cdn.styleref.io/b.webp"},
|
|
{"notaurl": True},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
)
|
|
assert inspiration_image_urls(style) == [
|
|
"https://cdn.styleref.io/a.webp",
|
|
"https://cdn.styleref.io/b.webp",
|
|
]
|
|
|
|
|
|
def test_inspiration_image_urls_empty_without_section():
|
|
from styleref_style import inspiration_image_urls
|
|
|
|
assert inspiration_image_urls(make_style(ref="x", name="Bare")) == []
|