Files
StyleRef-styleref-comfyui/tests/test_nodes.py
T
BehzadandClaude Sonnet 5 f8e5548b46 Drop internal plan references from code comments
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>
2026-07-26 23:18:25 +02:00

728 lines
27 KiB
Python

"""Node behaviour tests. HTTP is mocked throughout — no network in CI."""
from __future__ import annotations
import importlib.util
import math
import os
import pytest
import styleref_api as api
from styleref_api import StyleRefError
from styleref_nodes import apply as apply_node
from styleref_nodes import load as load_node
from styleref_nodes.login import StyleRefLogin
from styleref_style import make_style
STYLE = make_style(
ref="abc-123",
name="Real Moments",
author="@styleref",
url="https://styleref.io/share/abc-123",
sections={
"guardrails": {"values": {"avoided_visuals": "lens flares; film grain"}},
"colors": {"values": {"color_palette": {"solidColors": [{"hex": "#FFFFFF"}]}}},
},
)
@pytest.fixture(autouse=True)
def clear_cache():
load_node._CACHE.clear()
load_node._GENERATION.clear()
yield
load_node._CACHE.clear()
load_node._GENERATION.clear()
# ── pack registration ────────────────────────────────────────────────────────
def test_pack_registers_exactly_the_five_nodes():
"""
Extraction moved to the web app: the pack registers
Load/Apply/Facets/ReferenceImages/Login and nothing else — an Extract node
here means the removal regressed.
"""
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
spec = importlib.util.spec_from_file_location("styleref_pack", os.path.join(root, "__init__.py"))
pack = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pack)
assert set(pack.NODE_CLASS_MAPPINGS) == {
"StyleRefLoad",
"StyleRefApply",
"StyleRefFacets",
"StyleRefReferenceImages",
"StyleRefLogin",
}
assert set(pack.NODE_DISPLAY_NAME_MAPPINGS) == set(pack.NODE_CLASS_MAPPINGS)
# ── Apply: target mapping ────────────────────────────────────────────────────
def test_targets_are_exactly_the_web_apps_copy_list_in_order():
"""
Owner decision 2026-07-22: the target list mirrors the web
app's copy-box format list exactly — same entries, same order — so the
style means the same thing here as on styleref.io. Source of truth:
PROMPT_FORMAT_OPTIONS in src/components/ui/PromptFormatDropdown.tsx.
"""
assert list(apply_node.TARGETS) == [
"ai_tools",
"style_md",
"flux",
"midjourney",
"diffusion",
"json",
]
assert apply_node.TARGET_FORMATS == {
"ai_tools": "default",
"style_md": "stylemd",
"flux": "flux",
"midjourney": "midjourney",
"diffusion": "diffusion",
"json": "json",
}
def test_only_sampler_formats_escape_the_copy_out_badge():
"""flux and diffusion are the only targets that belong in a CLIP encoder."""
assert set(apply_node.COPY_OUT_TARGETS) == {"ai_tools", "style_md", "midjourney", "json"}
assert set(apply_node.TARGETS) - set(apply_node.COPY_OUT_TARGETS) == {"flux", "diffusion"}
assert set(apply_node.DOCUMENT_TARGETS) == {"style_md", "json"}
def test_every_advertised_target_has_a_format():
assert set(apply_node.TARGETS) == set(apply_node.TARGET_FORMATS)
# ── Apply: composition ───────────────────────────────────────────────────────
def test_subject_is_substituted_into_the_servers_slot():
"""
The compiled spec marks where the subject belongs for that target. Using the
slot beats any client-side ordering guess.
"""
style_text = f"Style: photography. {apply_node.SUBJECT_PLACEHOLDER}. Muted palette."
out = apply_node.compose_prompt("a lighthouse", style_text)
assert "a lighthouse" in out
assert apply_node.SUBJECT_PLACEHOLDER not in out
def test_placeholder_never_survives_into_a_prompt():
"""
The regression that matters: a leaked placeholder is encoded as literal text,
so the model is told to draw the words "YOUR SUBJECT".
"""
style_text = f"A photo. {apply_node.SUBJECT_PLACEHOLDER}. Natural light."
for subject in ("a lighthouse", "", " "):
assert "YOUR SUBJECT" not in apply_node.compose_prompt(subject, style_text)
def test_stripping_the_placeholder_leaves_clean_punctuation():
stripped = apply_node.strip_subject_placeholder(
f"A photo. {apply_node.SUBJECT_PLACEHOLDER}. Natural light."
)
assert stripped == "A photo. Natural light."
def test_subject_comes_first_when_there_is_no_slot():
"""Fallback ordering: early tokens carry the most weight."""
out = apply_node.compose_prompt("a lighthouse", "muted palette")
assert out.startswith("a lighthouse")
def test_subject_first_can_be_flipped():
out = apply_node.compose_prompt("a lighthouse", "muted palette", subject_first=False)
assert out.startswith("muted palette")
def test_composition_handles_an_empty_side():
assert apply_node.compose_prompt("", "style only") == "style only"
assert apply_node.compose_prompt("subject only", "") == "subject only"
def test_negative_merges_guardrails_and_extra_only():
"""
No injected boilerplate (owner decision 2026-07-22): the style's guardrails
are the authority, and anything generic belongs in extra_negative where it
stays visible.
"""
negative = apply_node.compose_negative(STYLE, "extra thing")
assert "lens flares" in negative
assert "extra thing" in negative
assert "watermark" not in negative, "no hidden base-negative boilerplate"
def test_negative_is_the_normalized_guardrail_terms_without_extra():
assert apply_node.compose_negative(STYLE, "") == "lens flares, film grain"
def test_negative_is_empty_for_a_style_without_guardrails():
assert apply_node.compose_negative(make_style(ref="x", name="Bare"), "") == ""
# ── Apply: caching ───────────────────────────────────────────────────────────
def test_compile_requests_the_raw_form(monkeypatch):
"""
The metadata header and attribution line are markdown for humans; sent to a
text encoder they are just wasted, misleading tokens.
"""
seen = {}
def fake_get(ref, fmt="default", compact=False, raw=False, sections=None):
seen["raw"] = raw
return "spec"
monkeypatch.setattr(api, "get_style_text", fake_get)
apply_node.compile_style(dict(STYLE, compiled={}), "flux", False)
assert seen["raw"] is True
def test_compile_keeps_the_header_for_style_md(monkeypatch):
"""STYLE.md is a document — its metadata header is part of it, not noise."""
seen = {}
def fake_get(ref, fmt="default", compact=False, raw=False, sections=None):
seen["fmt"], seen["raw"] = fmt, raw
return "# My Style — STYLE.md"
monkeypatch.setattr(api, "get_style_text", fake_get)
apply_node.compile_style(dict(STYLE, compiled={}), "style_md", False)
assert seen["fmt"] == "stylemd"
assert seen["raw"] is False
def test_style_md_target_ships_the_document_untouched(monkeypatch):
"""A subject must not be prepended to a STYLE.md document."""
doc = "# My Style\n\n## Colors\n- warm"
monkeypatch.setattr(api, "get_style_text", lambda *a, **k: doc)
node = apply_node.StyleRefApply()
out = node.apply(dict(STYLE, compiled={}), "a lighthouse", "style_md")
positive, _negative, _attr = out["result"]
assert positive == doc
def test_compiled_output_is_cached_per_target(monkeypatch):
"""Re-queueing with a new subject must not re-fetch an unchanged style."""
calls = {"n": 0}
def fake_get(ref, fmt="default", compact=False, raw=False, sections=None):
calls["n"] += 1
return f"compiled-{fmt}"
monkeypatch.setattr(api, "get_style_text", fake_get)
style = dict(STYLE, compiled={})
assert apply_node.compile_style(style, "flux", False) == "compiled-flux"
assert apply_node.compile_style(style, "flux", False) == "compiled-flux"
assert calls["n"] == 1, "second call should hit the cache"
# A different target is a genuinely different compilation.
apply_node.compile_style(style, "diffusion", False)
assert calls["n"] == 2
# …and so is compact, which is why it is part of the key.
apply_node.compile_style(style, "flux", True)
assert calls["n"] == 3
def test_apply_rejects_a_non_style_input():
node = apply_node.StyleRefApply()
with pytest.raises(StyleRefError, match="not a StyleRef style"):
node.apply({"nonsense": True}, "subject", "flux")
def test_apply_rejects_an_unloaded_style():
node = apply_node.StyleRefApply()
with pytest.raises(StyleRefError, match="No style is loaded"):
node.apply(make_style(ref="", name="(none)"), "subject", "flux")
# ── Apply: node-body feedback ────────────────────────────────────────────────
def test_apply_returns_ui_text_and_unchanged_result_tuple(monkeypatch):
"""The ui.text preview must not change the outputs' order or arity."""
monkeypatch.setattr(api, "get_style_text", lambda *a, **k: "muted palette, soft light")
node = apply_node.StyleRefApply()
out = node.apply(dict(STYLE, compiled={}), "a lighthouse", "diffusion")
assert set(out) == {"ui", "result"}
assert isinstance(out["ui"]["text"][0], str)
positive, negative, attribution_line = out["result"]
assert positive.startswith("a lighthouse")
assert negative == "lens flares, film grain"
assert "Real Moments" in attribution_line
def test_token_estimate_is_chars_over_four():
assert apply_node.estimate_tokens("x" * 400) == 100
def test_preview_shows_estimate_and_compact_hint_for_diffusion():
long_prompt = "word " * 200 # ~250 tokens
preview = apply_node.preview_text(long_prompt, "diffusion", compact=False)
assert "tokens (diffusion)" in preview
assert "consider `compact`" in preview
def test_preview_hint_is_absent_for_flux_and_when_already_compact():
long_prompt = "word " * 200
assert "consider" not in apply_node.preview_text(long_prompt, "flux", compact=False)
assert "consider" not in apply_node.preview_text(long_prompt, "diffusion", compact=True)
def test_preview_truncates_the_prompt_body():
preview = apply_node.preview_text("x" * 1000, "flux", compact=False)
first_line = preview.split("\n")[0]
assert len(first_line) <= apply_node.PREVIEW_CHARS + 1 # + ellipsis
# ── Load ─────────────────────────────────────────────────────────────────────
def test_load_requires_a_ref_or_a_search():
node = load_node.StyleRefLoad()
with pytest.raises(StyleRefError, match="No style reference"):
node.load("", "")
def test_fetch_style_caches_by_ref(monkeypatch):
calls = {"n": 0}
def fake_spec(ref, etag=None):
calls["n"] += 1
return {"name": "Cached", "sections": {}}, "https://styleref.io/share/abc", 'W/"e1"'
monkeypatch.setattr(api, "get_style_spec", fake_spec)
load_node.fetch_style("abc")
load_node.fetch_style("abc")
assert calls["n"] == 1
load_node.fetch_style("abc", use_cache=False)
assert calls["n"] == 2
def test_bump_refresh_drops_the_cache_and_dirties_is_changed(monkeypatch):
"""
The ↻ Refresh style button POSTs /styleref/refresh → bump_refresh(). That
must both forget the cached style AND change IS_CHANGED's key, or ComfyUI
would serve its own cached outputs without ever calling the node.
"""
calls = {"n": 0}
def fake_spec(ref, etag=None):
calls["n"] += 1
return {"name": "Fresh", "sections": {}}, None, 'W/"e1"'
monkeypatch.setattr(api, "get_style_spec", fake_spec)
load_node.fetch_style("abc")
load_node.fetch_style("abc")
assert calls["n"] == 1
key_before = load_node.StyleRefLoad.IS_CHANGED("abc")
load_node.bump_refresh("abc")
key_after = load_node.StyleRefLoad.IS_CHANGED("abc")
assert key_before != key_after
load_node.fetch_style("abc")
assert calls["n"] == 2
# No further bump → cached again.
load_node.fetch_style("abc")
assert calls["n"] == 2
def test_attribution_url_comes_from_the_servers_canonical_header(monkeypatch):
"""
The share URL is the server's, not a client guess: `{SITE_URL}/share/{ref}`
is only right for a bare share slug and breaks for an id or a URL ref.
"""
monkeypatch.setattr(
api,
"get_style_spec",
lambda ref, etag=None: ({"name": "Warm", "sections": {}}, "https://styleref.io/share/real-slug", None),
)
style = load_node.fetch_style("@ada/Warm Editorial")
assert style["url"] == "https://styleref.io/share/real-slug"
def test_private_refs_are_tagged_as_library(monkeypatch):
# Even when the server hands back a canonical URL, a private style is not
# attributed to a public link.
monkeypatch.setattr(
api,
"get_style_spec",
lambda ref, etag=None: ({"name": "Mine", "sections": {}}, "https://styleref.io/styles/abc", None),
)
style = load_node.fetch_style("Mine")
assert style["source"] == "library"
assert style["url"] is None
def test_ungenerated_style_error_reaches_the_user_verbatim(monkeypatch):
"""
When the server refuses a never-generated own-style ref (409),
its message — which tells the user to Generate on styleref.io — must pass
through unreworded.
"""
message = (
"This StyleRef hasn't been generated yet — open it on styleref.io and "
"Generate it first."
)
def raise_409(ref, etag=None):
raise StyleRefError(message, status=409, code="style_not_generated")
monkeypatch.setattr(api, "get_style_spec", raise_409)
node = load_node.StyleRefLoad()
with pytest.raises(StyleRefError) as caught:
node.load("Never Generated")
assert caught.value.message == message
def test_load_returns_ui_text_and_result_tuple(monkeypatch):
"""The load confirmation renders in the node body."""
monkeypatch.setattr(
api, "get_style_spec", lambda ref, etag=None: ({"name": "Warm", "sections": {}}, None, None)
)
node = load_node.StyleRefLoad()
out = node.load("abc")
assert set(out) == {"ui", "result"}
assert "Loaded" in out["ui"]["text"][0]
style, results = out["result"]
assert style["name"] == "Warm"
assert results == ""
def test_load_never_fetches_style_text(monkeypatch):
"""
STYLE.md moved to Apply's target list (owner decision 2026-07-22): Load has
no style_doc output and must never spend the extra text-fetch call.
"""
monkeypatch.setattr(
api, "get_style_spec", lambda ref, etag=None: ({"name": "Warm", "sections": {}}, None, None)
)
monkeypatch.setattr(
api,
"get_style_text",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("Load must not fetch text")),
)
load_node.StyleRefLoad().load("abc")
assert len(load_node.StyleRefLoad.RETURN_NAMES) == 2
def test_load_is_an_output_node():
"""A search-only Load must be queueable alone and still display."""
assert load_node.StyleRefLoad.OUTPUT_NODE is True
def test_search_results_render_slugs_to_copy():
text = load_node.format_search_results(
{"styles": [{"slug": "abc-1", "name": "Warm", "author": "@ada", "tags": ["warm", "film"]}]}
)
assert "abc-1" in text and "Warm" in text and "@ada" in text
assert "style_ref" in text, "instructions must name the field as it is named now"
def test_empty_search_suggests_categories():
text = load_node.format_search_results({"styles": [], "availableCategories": ["Photography"]})
assert "Photography" in text
assert "gallery" in text
def test_search_failure_does_not_sink_the_run(monkeypatch):
"""A bad search must not fail a graph whose ref is perfectly valid."""
monkeypatch.setattr(
api, "search_styles", lambda *a, **k: (_ for _ in ()).throw(StyleRefError("boom"))
)
monkeypatch.setattr(api, "get_style_spec", lambda ref, etag=None: ({"name": "Fine", "sections": {}}, None, None))
node = load_node.StyleRefLoad()
out = node.load("abc", "query that fails")
style, results = out["result"]
assert style["name"] == "Fine"
assert "Search failed" in results
def test_search_category_is_forwarded(monkeypatch):
"""The vanilla category combo must reach the API; `any` must not."""
seen = {}
def fake_search(query, category=None, sort=None, limit=12):
seen["category"] = category
return {"styles": []}
monkeypatch.setattr(api, "search_styles", fake_search)
monkeypatch.setattr(api, "get_style_spec", lambda ref, etag=None: ({"name": "X", "sections": {}}, None, None))
node = load_node.StyleRefLoad()
node.load("abc", "warm", category="Photography")
assert seen["category"] == "Photography"
node.load("abc", "warm", category="any")
assert seen["category"] is None
# ── IS_CHANGED contracts ─────────────────────────────────────────────────────
def test_load_is_changed_is_stable_when_cached():
key1 = load_node.StyleRefLoad.IS_CHANGED("abc", "q", "any", True)
key2 = load_node.StyleRefLoad.IS_CHANGED("abc", "q", "any", True)
assert key1 == key2
def test_load_is_changed_reports_dirty_without_cache():
value = load_node.StyleRefLoad.IS_CHANGED("abc", use_cache=False)
assert isinstance(value, float) and math.isnan(value)
def test_login_is_always_dirty():
"""A cached "signed out" status would be wrong the moment a sign-in succeeded."""
value = StyleRefLogin.IS_CHANGED()
assert isinstance(value, float) and math.isnan(value)
def test_apply_has_no_is_changed_override():
"""
Apply's dirtiness must come from its inputs alone. An IS_CHANGED override
returning NaN here is the credit-burn/api-hammer class of bug —
the node would re-run on every queue.
"""
assert "IS_CHANGED" not in vars(apply_node.StyleRefApply)
# ── Apply: sections filter ───────────────────────────────────────────────────
def test_sections_filter_reaches_the_api_and_the_cache_key(monkeypatch):
calls = []
def fake_get(ref, fmt="default", compact=False, raw=False, sections=None):
calls.append(sections)
return f"compiled:{sections}"
monkeypatch.setattr(api, "get_style_text", fake_get)
style = dict(STYLE, compiled={})
apply_node.compile_style(style, "diffusion", False, "colors, light_shadow")
assert calls[-1] == "colors,light_shadow"
# Same trimmed sections → cache hit; different sections → new compile.
apply_node.compile_style(style, "diffusion", False, "colors,light_shadow")
assert len(calls) == 1
apply_node.compile_style(style, "diffusion", False, "")
assert len(calls) == 2
assert calls[-1] is None
# ── Facets: full schema coverage ─────────────────────────────────────────────
def test_facets_outputs_are_exactly_the_schema_sections_plus_custom_items():
"""
Owner decision 2026-07-22: Facets mirrors the style board one-to-one — the
schema's section list in schema order, plus the six custom style items,
and NOTHING derived (no palette/primary_color/width/height/negative).
"""
from styleref_nodes import facets as facets_node
assert facets_node.SCHEMA_SECTIONS[0] == "output_format"
assert facets_node.SCHEMA_SECTIONS[-1] == "guardrails"
assert "inspiration_images" not in facets_node.SCHEMA_SECTIONS, (
"images are the Reference Images node's job"
)
names = facets_node.StyleRefFacets.RETURN_NAMES
assert names == tuple(facets_node.SCHEMA_SECTIONS) + tuple(facets_node.CUSTOM_ITEM_NAMES)
for derived in ("palette", "primary_color", "width", "height", "negative", "color_count"):
assert derived not in names
assert len(names) == len(facets_node.StyleRefFacets.RETURN_TYPES)
assert len(names) == len(facets_node.StyleRefFacets.OUTPUT_TOOLTIPS)
def test_facets_split_returns_one_value_per_output(monkeypatch):
from styleref_nodes import facets as facets_node
node = facets_node.StyleRefFacets()
out = node.split(STYLE)
assert len(out) == len(facets_node.StyleRefFacets.RETURN_NAMES)
by_name = dict(zip(facets_node.StyleRefFacets.RETURN_NAMES, out))
assert "avoided visuals: lens flares; film grain" in by_name["guardrails"]
assert "color palette" in by_name["colors"]
assert by_name["custom_style_item_3"] == ""
# ── Reference Images node ────────────────────────────────────────────────────
def _style_with_images(urls):
return make_style(
ref="img-style",
name="Imgs",
sections={
"inspiration_images": {
"values": {"inspiration_images": [{"url": u} for u in urls]}
}
},
)
def _png_bytes(width, height, color=(255, 0, 0)):
import io
from PIL import Image
buffer = io.BytesIO()
Image.new("RGB", (width, height), color).save(buffer, format="PNG")
return buffer.getvalue()
def test_reference_images_batches_and_counts(monkeypatch):
from styleref_nodes import reference_images as ri
ri._IMAGE_CACHE.clear()
blobs = {"https://a/1.png": _png_bytes(64, 32), "https://a/2.png": _png_bytes(100, 80)}
monkeypatch.setattr(api, "request_raw", lambda url, **k: (blobs[url], {}))
node = ri.StyleRefReferenceImages()
batch, count = node.fetch(_style_with_images(list(blobs)))
assert count == 2
# One batch = one resolution: the first image sets it, the rest resize.
assert batch.shape == (2, 32, 64, 3)
# IMAGE must be a torch tensor — PreviewImage/SaveImage call .cpu() on it.
# torch is not in the pack's dev extras, so this asserts only where it is
# installed; the node falls back to the ndarray otherwise.
torch = pytest.importorskip("torch")
assert isinstance(batch, torch.Tensor)
assert batch.dtype is torch.float32
assert float(batch.min()) >= 0.0 and float(batch.max()) <= 1.0
def test_reference_images_downloads_anonymously_and_caches(monkeypatch):
"""A bearer token must never go to storage hosts; repeats hit the cache."""
from styleref_nodes import reference_images as ri
ri._IMAGE_CACHE.clear()
calls = []
def fake_raw(url, anonymous=False, **_k):
calls.append(anonymous)
return _png_bytes(8, 8), {}
monkeypatch.setattr(api, "request_raw", fake_raw)
node = ri.StyleRefReferenceImages()
node.fetch(_style_with_images(["https://a/1.png"]))
node.fetch(_style_with_images(["https://a/1.png"]))
assert calls == [True], "one anonymous download, then the cache"
def test_reference_images_errors_clearly_without_images():
from styleref_nodes import reference_images as ri
with pytest.raises(StyleRefError, match="no inspiration images"):
ri.StyleRefReferenceImages().fetch(make_style(ref="x", name="Bare"))
# ── Load: save_to_library ────────────────────────────────────────────────────
def test_save_to_library_saves_once_per_session(monkeypatch):
calls = {"n": 0}
monkeypatch.setattr(
api, "get_style_spec", lambda ref, etag=None: ({"name": "Warm", "sections": {}}, None, None)
)
def fake_save(ref):
calls["n"] += 1
return {"saved": True, "alreadySaved": False}
monkeypatch.setattr(api, "save_style", fake_save)
load_node._SAVED.clear()
node = load_node.StyleRefLoad()
out = node.load("abc", save_to_library=True)
assert "Saved to your library" in out["ui"]["text"][0]
node.load("abc", save_to_library=True)
assert calls["n"] == 1, "idempotent server-side, but don't spam it either"
load_node._SAVED.clear()
def test_save_failure_does_not_sink_the_run(monkeypatch):
monkeypatch.setattr(
api, "get_style_spec", lambda ref, etag=None: ({"name": "Warm", "sections": {}}, None, None)
)
monkeypatch.setattr(
api, "save_style", lambda ref: (_ for _ in ()).throw(StyleRefError("Sign in first"))
)
load_node._SAVED.clear()
out = load_node.StyleRefLoad().load("abc", save_to_library=True)
style, _results = out["result"]
assert style["name"] == "Warm"
assert "Could not save" in out["ui"]["text"][0]
def test_own_styles_are_not_re_saved(monkeypatch):
# A private style canonicalises to the builder — that, not the ref, is what
# marks it as the caller's own.
monkeypatch.setattr(
api,
"get_style_spec",
lambda ref, etag=None: ({"name": "Mine", "sections": {}}, "https://styleref.io/styles/abc", None),
)
monkeypatch.setattr(
api, "save_style", lambda ref: (_ for _ in ()).throw(AssertionError("must not be called"))
)
out = load_node.StyleRefLoad().load("Mine", save_to_library=True)
assert "Already your own style" in out["ui"]["text"][0]
# ── Load: ETag revalidation ──────────────────────────────────────────────────
def test_cache_off_revalidates_with_the_stored_etag(monkeypatch):
"""use_cache=False means "re-check", not "re-download": a 304 keeps the
cached style — including its compiled-prompt cache."""
seen = []
def fake_spec(ref, etag=None):
seen.append(etag)
if etag == 'W/"e1"':
return None, None, etag # 304
return {"name": "Warm", "sections": {}}, None, 'W/"e1"'
monkeypatch.setattr(api, "get_style_spec", fake_spec)
first = load_node.fetch_style("abc")
first["compiled"]["diffusion:full:"] = "cached-compilation"
again = load_node.fetch_style("abc", use_cache=False)
assert seen == [None, 'W/"e1"']
assert again is first, "304 must keep the exact cached payload"
assert again["compiled"]["diffusion:full:"] == "cached-compilation"
def test_changed_style_replaces_the_cache_on_revalidation(monkeypatch):
def fake_spec(ref, etag=None):
if etag is None:
return {"name": "V1", "sections": {}}, None, 'W/"e1"'
return {"name": "V2", "sections": {}}, None, 'W/"e2"'
monkeypatch.setattr(api, "get_style_spec", fake_spec)
load_node.fetch_style("abc")
fresh = load_node.fetch_style("abc", use_cache=False)
assert fresh["name"] == "V2"